Какую проблему решают умные указатели
На предыдущей странице ты выделял память с помощью new и освобождал её через delete. Это работает, но возлагает бремя на тебя: каждому new нужен соответствующий delete на каждом пути выполнения кода, включая те, где на полпути выбрасывается исключение. Пропустишь один — получишь утечку памяти; вызовешь delete дважды — повредишь кучу.
Умные указатели решают это, привязывая время жизни памяти в куче к обычному объекту на стеке. Когда этот объект выходит из области видимости, его деструктор вызывает delete за тебя — гарантированно, даже если исключение раскручивает стек. Эта идея называется RAII (Resource Acquisition Is Initialization), а умные указатели находятся в заголовке <memory>.
Ты используешь *p и p->member точно так же, как у сырого указателя. Разница в том, что ты никогда не вызываешь delete — это делает умный указатель.
unique_ptr: один владелец, без разделения
unique_ptr — это умный указатель, к которому стоит обращаться по умолчанию. Он представляет эксклюзивное владение: ровно один unique_ptr владеет объектом в каждый момент времени, и когда этот указатель уничтожается, объект уничтожается вместе с ним. По сравнению с сырым указателем у него нулевые накладные расходы во время выполнения.
Создай его с помощью make_unique (C++14). Он принимает аргументы конструктора и возвращает тебе готовый к использованию указатель:
Поскольку владелец может быть только один, unique_ptr нельзя копировать. Попытка скопировать его — это ошибка компиляции, и эта ошибка — то, как язык защищает тебя от двух владельцев, каждый из которых пытается вызвать delete для одного и того же объекта:
auto a = make_unique<int>(10);
auto b = a; // error: call to deleted copy constructor of unique_ptr
Чтобы передать владение кому-то другому, ты перемещаешь его с помощью std::move. После перемещения исходный указатель пуст (он содержит nullptr):
Это та модель, которая нужна тебе большую часть времени: всегда есть ровно один чёткий владелец, и компилятор это обеспечивает.
shared_ptr: совместное владение через подсчёт ссылок
Иногда нескольким частям твоей программы действительно нужно делить один и тот же объект, и ни одна из них не знает, какая завершится последней. Именно для этого нужен shared_ptr. Он ведёт счётчик ссылок: каждая копия увеличивает счётчик, каждое уничтожение уменьшает его, и объект освобождается только тогда, когда счётчик достигает нуля.
Создавай их с помощью make_shared:
В отличие от unique_ptr, копировать shared_ptr можно — в этом и весь смысл. Цена за это — стоимость: счётчик ссылок хранится в куче и обновляется атомарно (потокобезопасно), поэтому shared_ptr тяжелее, чем unique_ptr. Прибегай к нему только тогда, когда владение действительно совместное, а не просто чтобы не думать о том, кто чем владеет.
make_shared к тому же эффективнее, чем shared_ptr<T>(new T(...)): он выделяет объект и управляющий блок за одно выделение вместо двух.
weak_ptr и разрыв циклов ссылок
У shared_ptr есть одна классическая ловушка: если два объекта держат shared_ptr друг на друга, их счётчики ссылок никогда не достигают нуля, поэтому ни один из них не освобождается — утечка памяти, несмотря на то что ты использовал умные указатели.
struct Node {
shared_ptr<Node> next; // если два узла указывают друг на друга,
}; // они вечно держат друг друга живыми
Решение — weak_ptr: не владеющий наблюдатель за shared_ptr. Он не увеличивает счётчик ссылок, поэтому никогда не удерживает объект живым. Чтобы воспользоваться объектом, ты вызываешь .lock(), который возвращает тебе shared_ptr, если объект ещё существует, или пустой указатель, если его уже нет.
Используй weak_ptr для «обратных указателей» и кэшей — везде, где хочешь ссылаться на объект, не претендуя на владение им.
Частые ошибки и подводные камни
Умные указатели устраняют большинство ошибок с памятью, но несколько ловушек остаётся:
Не смешивай умное и сырое владение одной и той же памятью. Никогда не создавай два умных указателя из одного сырого указателя — каждый попытается вызвать для него delete:
int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw); // катастрофа: оба удалят один и тот же int (двойное освобождение)
Именно поэтому ты предпочитаешь make_unique/make_shared — нет болтающегося сырого указателя, который можно было бы использовать неправильно.
unique_ptr можно только перемещать, поэтому передавай его по значению, чтобы передать владение. Если функция должна использовать, но не владеть объектом, принимай вместо этого обычную ссылку или сырой T* — сырой указатель, который лишь наблюдает, совершенно нормален:
void consume(unique_ptr<int> p); // забирает владение (перемещение внутрь)
void observe(int* p); // только смотрит, ничем не владеет
Не хватайся за shared_ptr по умолчанию. Это соблазнительно, потому что он свободно копируется, но атомарный подсчёт ссылок стоит реальной производительности, а совместное владение усложняет рассуждения о времени жизни. По умолчанию используй unique_ptr; переходи к shared_ptr только тогда, когда тебе действительно нужно несколько владельцев.
unique_ptr для массивов требует формы массива. make_unique<int[]>(n) даёт тебе unique_ptr<int[]>, который корректно вызывает delete[]. На практике для динамических массивов предпочитай std::vector — он управляет памятью за тебя и вдобавок отслеживает размер.
Далее: строки
Теперь управление памятью под контролем: умные указатели дают тебе выделение в куче без утечек. Одна из самых частых вещей, которые ты будешь выделять и передавать, — это текст, и C++ даёт тебе куда более безопасный инструмент, чем сырые буферы char*. Следующая страница посвящена std::string: как он растёт сам по себе, какие операции ты будешь использовать каждый день и почему он полностью освобождает тебя от ручной работы с памятью.
Часто задаваемые вопросы
Что такое умные указатели в C++?
Умные указатели — это объекты из заголовка <memory> (unique_ptr, shared_ptr, weak_ptr), которые оборачивают сырой указатель и автоматически вызывают delete для памяти, когда выходят из области видимости. Они дают тебе выделение в куче без ручного delete и без утечек, которые возникают, если о нём забыть.
В чём разница между unique_ptr и shared_ptr?
unique_ptr — единственный владелец своего объекта: его нельзя копировать, только перемещать, и он освобождает память в тот момент, когда уничтожается. shared_ptr допускает совместное владение через подсчёт ссылок: на один и тот же объект могут указывать несколько shared_ptr, и объект освобождается только тогда, когда уничтожается последний. Предпочитай unique_ptr, если тебе действительно не нужно совместное владение.
Что использовать в современном C++ — make_unique или new?
Используй make_unique и make_shared. Они выделяют и оборачивают объект за один шаг, поэтому нет сырого new, результат которого мог бы утечь, прежде чем попадёт в умный указатель. Как правило, в современной кодовой базе на C++ почти не должно быть голых new или delete.