Умные указатели — владение, выраженное в типе
Управление памятью вручную в C++ выглядит просто — пока не появляются ранние return, исключения и развилки. Один забытый delete — утечка; один лишний — UB; два владельца одного объекта — double free. Корень всех этих ошибок один: ресурс и ответственность за его освобождение существуют отдельно. Один — в куче, другой — в голове разработчика.
Смарт-указатели решают это через RAII: ресурс захватывается в конструкторе, освобождается в деструкторе, а деструктор вызывается автоматически на любом пути выхода из scope — через return, через исключение, через ранний break. Ответственность перемещается из документации в тип.
Главное, что даёт смарт-указатель — не «автоматическое удаление», а видимое в сигнатуре владение:
std::unique_ptr<T>в параметре функции — это контракт «передача владения».const T&— «только наблюдение, объект обязательно жив».T*илиstd::weak_ptr<T>— «может быть nullptr или мёртвым, нужно проверить».
Перевыбрать тип — значит сменить смысл интерфейса. Полная карта моделей владения — в слоях ниже.
Карта темы
- std::unique_ptr — единоличный владелец — нулевые накладные расходы, move-only семантика, custom deleter,
releasevsreset. - std::shared_ptr и control block — разделённое владение, control block,
make_sharedvsnew, потокобезопасность счётчика vs объекта. - std::weak_ptr — наблюдатель без владения —
lockvsexpired, разрыв цикловshared_ptr, паттерн в async-callbacks. - enable_shared_from_this и self-shared_ptr — почему
shared_ptr<T>(this)— UB, как метод возвращаетshared_ptrна себя. - Custom deleters и RAII для не-памяти —
FILE*, OS-хендлы,malloc/free,std::out_ptrиз C++23. - boost::intrusive_ptr и intrusive counting — счётчик внутри объекта, COM/Qt/Unreal, отличия от
shared_ptr.
Краткое сравнение
unique_ptr | shared_ptr | weak_ptr | intrusive_ptr | Raw T* | |
|---|---|---|---|---|---|
| Владение | Единоличное | Разделённое | Нет | Разделённое | Нет |
| Копирование | Нет (move-only) | Да | Да | Да | Да |
| Control block | Нет | Да | Да (через shared_ptr) | Нет — счётчик в объекте | Нет |
| Накладные расходы | ~нулевые | Атомарные счётчики | Атомарные на lock | Атомарные счётчики | Нет |
| Основной сценарий | Владелец по умолчанию | Shared lifetime | Observer / разрыв циклов | Legacy / framework | Наблюдение, interop |
Правило выбора: по умолчанию unique_ptr. shared_ptr — только если разделённое владение действительно часть модели предметной области, а не способ «не думать, кто удаляет».
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
shared_ptr<T>(this) в методе | Два независимых control block → double free, UB |
Два shared_ptr из одного raw pointer | То же самое — два control block, double free |
Цикл shared_ptr (Parent ↔ Child) | Утечка — strong count никогда не упадёт до 0 |
Захват shared_ptr в lambda async-callback | Объект живёт сколько живёт callback — неожиданный lifetime |
Считать shared_ptr потокобезопасным для объекта | Только счётчик атомарен; объекту нужна обычная синхронизация |
release() без немедленного delete | Утечка — unique_ptr отдал владение, никто его не подобрал |
shared_from_this() на необёрнутом объекте | std::bad_weak_ptr (C++17) или UB (раньше) |
delete на памяти из malloc через дефолтный deleter | UB; нужен custom deleter |
unique_ptr<T> на new T[] | UB при delete; нужен unique_ptr<T[]> |
Логика «жив ли объект» через use_count() | Хрупко и race-prone; используйте weak_ptr::lock |
Значение для собеседований
Умные указатели — один из самых частых топиков на C++-собеседованиях, от junior до staff. Кандидат, который знает только «умный указатель сам удаляет», обычно проваливается на первом уточняющем вопросе.
Что обычно проверяют:
- Понимание модели владения: unique vs shared vs observing.
- Что такое control block — что хранит, где живёт, когда удаляется.
make_sharedvsshared_ptr(new T)— аллокации, exception safety, нюанс с памятью при долгоживущихweak_ptr.- Циклические
shared_ptr— классический вопрос-ловушка. weak_ptr:lock()vsexpired(), паттерн в callbacks.enable_shared_from_this: когда нужен, что произойдёт без него.- Потокобезопасность: счётчик атомарный, объект — нет.
- Когда уместен custom deleter и intrusive_ptr.
Типичный неверный ответ: «Умные указатели предотвращают все утечки памяти». Правильно — предотвращают, если нет циклов через shared_ptr и если владение действительно выражено в типе. Пример с циклом Parent/Child — это не баг компилятора, это баг дизайна владения, который смарт-указатели не лечат сами.