Умные указатели в C++: понятное руководство по unique_ptr, shared_ptr и weak_ptr с примерами

Запрос «умные указатели в C++ примеры» стабильно популярен: новичкам сложно сразу понять, чем отличаются unique_ptr, shared_ptr и weak_ptr, а продолжающим — где их применять без лишнего оверхеда. В этом руководстве мы разберем каждую модель владения, типичные ошибки и покажем рабочие примеры, которые пригодятся в реальных проектах.
Что решают умные указатели в C++
Умные указатели — это RAII-обертки вокруг «сырых» указателей (raw pointers), которые автоматически освобождают ресурсы при выходе из области видимости. Это снижает риск утечек памяти, упрощает владение и делает код безопаснее. Главные герои: std::unique_ptr, std::shared_ptr и std::weak_ptr.
std::unique_ptr: единственный владелец
unique_ptr гарантирует уникальное владение объектом. Нельзя копировать, но можно перемещать. Идеален по умолчанию, если объектом владеет ровно одна сущность.
#include <memory>
#include <iostream>
#include <vector>
struct Widget {
int id;
explicit Widget(int i) : id(i) { std::cout << "Widget(" << id << ")\n"; }
~Widget() { std::cout << "~Widget(" << id << ")\n"; }
};
int main() {
auto p = std::make_unique<Widget>(42); // создание
std::vector<std::unique_ptr<Widget>> v;
v.push_back(std::move(p)); // перенос владения в вектор
// p теперь пуст (nullptr)
}
Передача владения в функцию — по значению unique_ptr (с перемещением):
void take(std::unique_ptr<Widget> w) {
// w - новый владелец
}
void use(const Widget& w) {
// только пользуемся, не владеем
}
int main() {
auto p = std::make_unique<Widget>(1);
use(*p);
take(std::move(p));
}
Можно хранить в контейнерах, использовать полиморфизм и безопасно управлять массивами:
struct Base { virtual ~Base() = default; virtual void f() = 0; };
struct Derived : Base { void f() override { std::cout << "Derived\n"; } };
int main() {
std::unique_ptr<Base> pb = std::make_unique<Derived>();
pb->f();
auto arr = std::make_unique<int[]>(5); // динамический массив
arr[0] = 10;
}
Кастомные делетеры делают unique_ptr универсальным для системных ресурсов:
#include <cstdio>
using FilePtr = std::unique_ptr<FILE, decltype(&std::fclose)>;
FilePtr openFile(const char* path) {
FILE* f = std::fopen(path, "r");
return FilePtr(f, &std::fclose);
}
std::shared_ptr: разделяемое владение
shared_ptr считает количество владельцев и освобождает ресурс, когда счетчик доходит до нуля. Удобен, когда объект делят несколько частей системы (например, граф сцены, кэш, пул).
#include <memory>
#include <iostream>
struct Data { int v; explicit Data(int x) : v(x) {} };
int main() {
auto a = std::make_shared<Data>(10);
auto b = a; // еще один владелец
std::cout << a.use_count() << " owners\n"; // 2
b.reset();
std::cout << a.use_count() << " owners\n"; // 1
}
Важно: увеличение/уменьшение счетчика потокобезопасно, но сам объект нет. Если объект используется из нескольких потоков, защитите доступ мьютексами.
std::weak_ptr: наблюдатель без владения
weak_ptr не продлевает жизнь объекта. Используется, чтобы разорвать циклы владения и устраивать безопасные ссылки-наблюдатели.
#include <memory>
#include <iostream>
int main() {
std::weak_ptr<int> wp;
{
auto sp = std::make_shared<int>(5);
wp = sp; // наблюдаем за sp
if (auto locked = wp.lock()) {
std::cout << *locked << "\n";
}
} // sp уничтожен
if (auto locked = wp.lock()) {
std::cout << *locked << "\n";
} else {
std::cout << "expired\n";
}
}
Циклические ссылки: типичная ловушка shared_ptr
Два объекта, ссылающихся друг на друга shared_ptr-ами, никогда не освободятся — счетчики не станут нулем. Решение: одна сторона хранит weak_ptr.
#include <memory>
struct Node {
int value{};
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // разрываем цикл слабой ссылкой
};
shared_from_this: безопасно получить shared_ptr из this
Если объект уже управляется shared_ptr, а вам нужен еще один shared_ptr на него внутри метода — используйте enable_shared_from_this.
#include <memory>
#include <iostream>
struct Session : std::enable_shared_from_this<Session> {
void start() {
auto self = shared_from_this(); // безопасно
std::cout << "owners: " << self.use_count() << "\n";
}
};
int main() {
auto s = std::make_shared<Session>();
s->start();
}
Нельзя вызывать shared_from_this(), если объект не был создан под управлением shared_ptr — будет исключение.
Как передавать умные указатели в функции
- Нужно просто «пользоваться» объектом: const T& или T& (без владения).
- Нужно передать владение единственному получателю: std::unique_ptr<T> по значению (вызывать std::move при вызове).
- Нужно разделить владение: std::shared_ptr<T> по значению или по const std::shared_ptr<T>& для экономии инкремента/декремента счетчика при частых вызовах.
- Нужно «слабое» наблюдение: std::weak_ptr<T> и внутри функции lock().
- Сырые указатели T* годятся только как невладеющие ссылки для совместимости с C-APIs, но в современном коде предпочтительнее ссылки &.
Производительность и практические советы
- std::make_unique и std::make_shared — предпочтительные фабрики. make_shared делает одну аллокацию для объекта и контрольного блока, что быстрее и кеш-дружелюбнее.
- Не смешивайте владение: если у вас есть shared_ptr<T>, не создавайте параллельный unique_ptr<T> на тот же объект — поведение неопределено.
- Не «делитесь» владением через get(): сырые указатели из get() не увеличивают счетчик, легко получить use-after-free.
- Кастомный делетер поддерживается в shared_ptr через конструктор, но не поддерживается в make_shared. Если нужен делетер — используйте shared_ptr<T>(new T, Deleter).
- Для наблюдателей в паттерне «наблюдатель» используйте weak_ptr, чтобы не держать объект вечно живым.
Мини-чеклист выбора
- Владение одно: unique_ptr (по умолчанию).
- Владение разделяется: shared_ptr (осознанно, за счет накладных расходов).
- Наблюдение без владения: weak_ptr.
- Нужны массивы: unique_ptr<T[]> или контейнеры STL (vector, string, и т.д.).
- Нужна скорость и простота — избегайте shared_ptr без необходимости.
Типичные вопросы на собеседовании
- Чем отличается unique_ptr от shared_ptr по затратам? (shared_ptr хранит контрольный блок и атомарно меняет счетчик — дороже).
- Когда нужен weak_ptr? (разрыв циклов и наблюдение без продления жизни).
- Почему стоит предпочитать make_unique/make_shared? (исключительная безопасность и меньше аллокаций).
- Как передавать unique_ptr в функцию? (по значению + std::move).
Где потренироваться и закрыть пробелы
Если хотите быстро отточить навыки и пройти практику с задачами и разбором, рекомендую посмотреть программу курса: Практический интенсив «Программирование на C++ с Нуля до Гуру» — стартуйте и прокачайте C++ до уровня проектов.
Итоги
Умные указатели в C++ — базовый инструмент современного кода. Используйте unique_ptr по умолчанию, подключайте shared_ptr только при реальной необходимости разделения владения, а weak_ptr — для безопасных наблюдений и разрыва циклов. Следуя этим правилам и примерам, вы избежите утечек памяти, упростите дизайн и сделаете код надежнее и быстрее.
-
-
Михаил Русаков
Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.