Умные указатели в C++: когда unique, когда shared, когда weak
unique_ptr почти бесплатный, shared_ptr тянет за собой атомарный счётчик и лишнюю аллокацию, а weak_ptr существует, чтобы разорвать циклы. Разбираем, что выбирать под конкретную задачу и чем платите за каждый вариант.

Проект утекает памятью, и профилировщик показывает объекты, которые должны были умереть полчаса назад. В коде — shared_ptr на каждом углу, включая места, где владелец очевидно один. Плюс два узла графа держат друг друга через shared_ptr, и счётчик ссылок у обоих никогда не падает до нуля. Обе проблемы решаются, если понимать цену каждого из трёх умных указателей, а не ставить shared_ptr по привычке.
Ниже — разбор для C++17 и новее (весь код собирается с -std=c++17). Речь про владение динамической памятью: кто отвечает за удаление объекта и сколько это стоит. Реализация счётчиков и вопросы lock-free оставим за скобками — здесь только выбор инструмента под задачу.
Три указателя и одна идея
Все три типа живут в <memory> и решают одну задачу — привязать время жизни объекта к времени жизни владельца, чтобы не звать delete руками. Отличаются они моделью владения.
- unique_ptr — единственный владелец. Копировать нельзя, можно только перемещать. Удаляет объект в деструкторе.
- shared_ptr — совместное владение. Считает ссылки; объект умирает, когда счётчик доходит до нуля.
- weak_ptr — наблюдатель без владения. Не продлевает жизнь объекту, но умеет проверить, жив ли он ещё, и получить временный
shared_ptr.
Ключевой вопрос при выборе — не «сколько кода придётся написать», а «сколько владельцев у объекта на самом деле». В 90% случаев владелец один, и это unique_ptr.
unique_ptr: дефолт, а не крайний случай
По размеру unique_ptr с обычным делитером равен голому указателю — 8 байт на 64-битной системе. Никакого счётчика, никакой синхронизации, деструктор просто зовёт delete. Создавайте через make_unique (появился в C++14): это одна аллокация и защита от утечки при исключении в списке аргументов.
#include <memory>
#include <vector>
struct Connection {
explicit Connection(int fd) : fd_(fd) {}
~Connection() { /* закрываем сокет */ }
int fd_;
};
// Владелец один — фабрика создаёт, вызывающий забирает.
std::unique_ptr<Connection> open_connection(int fd) {
return std::make_unique<Connection>(fd);
}
void use() {
auto conn = open_connection(42);
// conn удалится автоматически при выходе из области видимости
// передать владение дальше можно только через std::move(conn)
}Возврат unique_ptr из функции — идиоматичный способ сказать «я создал объект, теперь он твой». Компилятор оптимизирует перемещение, копирования тут нет.
shared_ptr: за совместное владение платят
Как только вы пишете shared_ptr, появляется управляющий блок (control block) со счётчиком ссылок. Он живёт в куче отдельно от объекта — если создать через конструктор от готового указателя. Поэтому используйте make_shared: он размещает объект и управляющий блок одной аллокацией.
// Плохо: две аллокации, объект и control block в разных местах
std::shared_ptr<Connection> a(new Connection(1));
// Хорошо: одна аллокация
auto b = std::make_shared<Connection>(2);
// Копия увеличивает счётчик атомарно
auto c = b; // теперь use_count() == 2За что вы платите с shared_ptr:
- сам объект занимает 16 байт вместо 8 — это два указателя (на объект и на control block);
- инкремент и декремент счётчика — атомарные операции. Даже если объект никогда не шарится между потоками, вы платите за атомарность;
make_sharedэкономит на аллокации, но объект и control block живут одним куском памяти — пока хоть одинweak_ptrжив, память под объект не освобождается.
Если вы не можете назвать второго владельца объекта, вам не нужен shared_ptr. Совместное владение — это ответ на конкретный вопрос «кто ещё держит эту вещь», а не стиль на всякий случай.
weak_ptr: лекарство от циклов
Два shared_ptr, указывающие друг на друга, — классическая утечка. Счётчик каждого никогда не дойдёт до нуля, потому что второй держит первый живым, и наоборот. weak_ptr наблюдает за объектом, но не увеличивает счётчик владельцев.
#include <memory>
struct Node {
std::shared_ptr<Node> next; // владеет следующим
std::weak_ptr<Node> prev; // только наблюдает за предыдущим
};
void link() {
auto first = std::make_shared<Node>();
auto second = std::make_shared<Node>();
first->next = second; // second.use_count() == 2
second->prev = first; // first.use_count() остаётся 1, prev — weak
// при выходе из функции оба узла корректно удалятся
}Чтобы обратиться к объекту через weak_ptr, вызовите lock() — он вернёт shared_ptr (пустой, если объект уже мёртв). Проверять живость через expired() и потом разыменовывать нельзя: между проверкой и доступом объект в другом потоке может умереть.
if (auto locked = weak.lock()) {
// locked — валидный shared_ptr, объект жив, счётчик +1 на время блока
use(*locked);
} else {
// объект уже удалён
}Что выбирать под задачу
| Ситуация | Тип | Почему |
|---|---|---|
| Владелец очевидно один, объект передаётся по цепочке | unique_ptr | Нулевые накладные расходы, ясная семантика передачи |
| Объект честно делят несколько независимых частей кода | shared_ptr | Никто не знает, кто уйдёт последним |
| Кэш, обратная ссылка, наблюдатель | weak_ptr | Не должен продлевать жизнь объекту, разрывает циклы |
| Функция читает объект, не забирая владение | сырой указатель или ссылка | Владение не меняется — умный указатель тут лишний |
Отдельный частый промах — передавать shared_ptr в функцию по значению, когда функция объект только читает. Это лишний инкремент/декремент счётчика на каждый вызов. Если функция не хранит указатель дольше своего вызова, принимайте const T& или сырой T*.
Что ломается на практике
- Двойное владение из сырого указателя. Два
shared_ptr, созданных из одногоnew, заводят два независимых control block. Оба попробуют удалить объект — undefined behavior. Никогда не создавайтеshared_ptrиз указателя, который уже кому-то принадлежит. - this как shared_ptr. Если объект уже управляется
shared_ptr, а метод хочет отдатьshared_ptrна себя, наследуйте отstd::enable_shared_from_thisи зовитеshared_from_this(). Обернутьthisв новыйshared_ptr— та же ошибка с двумя control block. - Цикл, который не заметили. Родитель держит детей через
shared_ptr, ребёнок держит родителя тоже черезshared_ptr— утечка. Обратная ссылка почти всегда должна бытьweak_ptr. - Кастомный делитер и make-функции.
make_uniqueиmake_sharedне умеют принимать свой делитер. Если объект удаляется не черезdelete(например, освобождается C-функцией), придётся конструировать указатель напрямую и передавать делитер вторым аргументом. - Атомарность счётчика ≠ потокобезопасность объекта. Копировать один
shared_ptrиз разных потоков безопасно — счётчик атомарный. Но доступ к самому объекту через указатель нужно синхронизировать самому.
Что делать дальше
Пройдитесь по коду и замените shared_ptr на unique_ptr везде, где владелец один — это уберёт лишние атомарные операции и сделает намерение явным. Для поиска циклов включите санитайзеры при сборке (-fsanitize=address у Clang и GCC) — утечки от циклических shared_ptr они видят. В проде смотрите на рост use_count() у долгоживущих объектов: если счётчик растёт и не падает, где-то забыли сделать ссылку слабой. Точные гарантии по каждому методу — в разделе smart pointers на cppreference для вашей версии стандарта.
Задачи: математика, физика, химия: Разбор задачи по шагам с проверкой и похожей задачей
Спросить за 10 ₽Источники: cppreference — Dynamic memory management (smart pointers), ISO C++ Core Guidelines — Resource management
Частые вопросы
Что использовать по умолчанию, если непонятно, какой указатель нужен?
У меня старый компилятор без C++17, эти примеры соберутся?
Почему make_shared лучше конструктора shared_ptr от new?
Опасно ли передавать shared_ptr между потоками?
Как понять, что у меня утечка из-за циклической ссылки?
Нужен ли умный указатель, если функция только читает объект и не хранит его?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.