Введение
Мы разбирали ручное управление динамической памятью через new/delete, систематически разберёт RAII как общую философию — умные указатели представляют практическое, готовое к использованию воплощение RAII конкретно для управления динамически выделенными объектами, автоматизируя освобождение памяти и существенно снижая риск утечек и ошибок ручного управления.
Концепция
std::unique_ptr<T> — умный указатель, обеспечивающий единственное, эксклюзивное владение динамически выделенным объектом — память автоматически освобождается через деструктор unique_ptr, когда сам unique_ptr уничтожается (выход из области видимости), и unique_ptr не может быть скопирован (только перемещён), что и обеспечивает гарантию единственности владельца на уровне системы типов. std::shared_ptr<T> поддерживает совместное владение объектом через внутренний счётчик ссылок — память освобождается только когда последний shared_ptr, ссылающийся на этот объект, уничтожается (счётчик ссылок доходит до нуля), что даёт большую гибкость по сравнению с unique_ptr, но за счёт дополнительных накладных расходов на поддержание этого счётчика. std::weak_ptr<T> — «слабая» ссылка на объект, управляемый shared_ptr, не влияющая на счётчик ссылок и, соответственно, не продлевающая время жизни объекта — используется для разрыва циклических ссылок между shared_ptr (статья ниже о подводных камнях) и для безопасной проверки, существует ли объект ещё в данный момент, без гарантированного владения им.
Пример кода
#include <iostream>
#include <memory>
class Resource {
public:
Resource(int id) : m_id(id) { std::cout << "Resource " << m_id << " создан" << std::endl; }
~Resource() { std::cout << "Resource " << m_id << " уничтожен" << std::endl; }
private:
int m_id;
};
int main()
{
{
std::unique_ptr<Resource> ptr = std::make_unique<Resource>(1);
// НЕЛЬЗЯ скопировать: std::unique_ptr<Resource> ptr2 = ptr; // ОШИБКА КОМПИЛЯЦИИ
std::unique_ptr<Resource> ptr2 = std::move(ptr); // МОЖНО переместить (статья 546)
} // Resource 1 автоматически уничтожен ЗДЕСЬ, при выходе из блока — НЕ требуется явный delete!
return 0;
}
// shared_ptr — совместное владение через счётчик ссылок
#include <iostream>
#include <memory>
int main()
{
std::shared_ptr<Resource> ptr1 = std::make_shared<Resource>(2);
std::cout << "Счётчик ссылок: " << ptr1.use_count() << std::endl; // 1
{
std::shared_ptr<Resource> ptr2 = ptr1; // КОПИРОВАНИЕ shared_ptr — допустимо, увеличивает счётчик
std::cout << "Счётчик ссылок: " << ptr1.use_count() << std::endl; // 2
} // ptr2 уничтожен здесь — счётчик уменьшается, но Resource ЕЩЁ ЖИВ, поскольку ptr1 всё ещё существует
std::cout << "Счётчик ссылок: " << ptr1.use_count() << std::endl; // 1
return 0;
} // Resource 2 уничтожается ЗДЕСЬ, когда ptr1 (последний владелец) выходит из области видимости
// weak_ptr — разрыв циклической ссылки между двумя shared_ptr, ссылающимися друг на друга
#include <memory>
#include <iostream>
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> previous; // weak_ptr вместо shared_ptr — РАЗРЫВАЕТ потенциальный цикл ссылок
int value;
};
int main()
{
auto first = std::make_shared<Node>();
auto second = std::make_shared<Node>();
first->next = second;
second->previous = first; // weak_ptr — НЕ увеличивает счётчик ссылок first, предотвращая утечку памяти
// Без weak_ptr (если бы previous был shared_ptr) — first и second держали бы друг друга
// "живыми" БЕСКОНЕЧНО, даже после выхода из области видимости — классическая утечка через цикл
return 0;
}
Пояснения к коду
std::unique_ptr<Resource> ptr с попыткой копирования (закомментированной как ошибка компиляции) и реально работающим перемещением (std::move(ptr)) показывает фундаментальное ограничение unique_ptr, гарантирующее единственность владения на уровне системы типов — компилятор сам предотвращает ситуацию, где два разных unique_ptr могли бы случайно ссылаться на один и тот же объект, что устранило бы саму гарантию единственного владельца. Демонстрация счётчика ссылок (use_count()) для shared_ptr показывает реальный механизм совместного владения — каждое копирование shared_ptr увеличивает счётчик, каждое уничтожение копии уменьшает его, и реальное освобождение управляемого объекта Resource происходит только тогда, когда счётчик достигает нуля (последний владелец уничтожен), что демонстрируется тем, что Resource 2 продолжает существовать даже после уничтожения ptr2, пока жив ptr1. Пример с Node::previous как weak_ptr (вместо потенциально проблематичного shared_ptr) показывает классическое практическое применение weak_ptr — без этого изменения два узла, ссылающиеся друг на друга через shared_ptr в обоих направлениях, образовали бы циклическую зависимость, в которой счётчик ссылок каждого из них никогда не достигнет нуля (поскольку каждый держит другого «живым»), приводя к перманентной утечке памяти даже после того, как внешний код потерял все остальные ссылки на оба узла.
Подводные камни
- Использование
shared_ptr«по умолчанию» вместоunique_ptrбез действительной необходимости в совместном владении —shared_ptrнесёт дополнительные накладные расходы (поддержание атомарного, потокобезопасного счётчика ссылок) по сравнению сunique_ptr, и для случаев, где объект реально имеет единственного, чётко определённого владельца (что является значительно более распространённой, типичной ситуацией, чем реальная потребность в совместном владении),unique_ptr— более эффективный и более явно документирующий реальное предназначение код выбор, иshared_ptrстоит использовать осознанно, именно когда совместное владение действительно нужно. - Создание циклических ссылок через
shared_ptrбез использованияweak_ptrдля разрыва цикла, как явно демонстрирует пример сNode— это одна из главных, классических опасностейshared_ptr, приводящая к утечкам памяти, не обнаруживаемым автоматическим механизмом подсчёта ссылок самим по себе (поскольку счётчик каждого узла в цикле никогда не достигает нуля), и любая структура данных с потенциальными циклическими связями между объектами, управляемымиshared_ptr(двусвязные списки, древовидные структуры со ссылками «родитель-потомок» в обоих направлениях), должна явно использоватьweak_ptrдля одного из направлений связи, разрывающего потенциальный цикл. - Создание
shared_ptrиз «сырого» указателя несколько раз для того же самого объекта (std::shared_ptr<Resource> a(rawPtr); std::shared_ptr<Resource> b(rawPtr);для одного и того жеrawPtr) вместо копирования уже существующегоshared_ptr— каждое такое отдельное создание из «сырого» указателя создаёт НЕЗАВИСИМЫЙ счётчик ссылок, не связанный с другими, что приводит к множественному, несогласованному освобождению одного и того же объекта (как только первый из независимых счётчиков достигает нуля), являющемуся классической ошибкой двойного освобождения, и правильный способ совместного владения — копирование уже существующегоshared_ptr(какptr2 = ptr1в примере), а не повторное, независимое создание из исходного «сырого» указателя. - Попытка использовать
weak_ptrнапрямую, как обычный указатель, без предварительного преобразования вshared_ptrчерез методlock()—weak_ptrне предоставляет прямого доступа к управляемому объекту (через оператор*/->) именно потому, что объект может быть уже уничтожен к моменту попытки доступа, и корректное использование требует явного вызоваlock(), возвращающего временныйshared_ptr(либо валидный, если объект ещё существует, либо пустой/нулевой, если объект уже был уничтожен), что нужно явно проверить перед фактическим использованием полученного результата.