Зачем нужны умные указатели
«Сырые» указатели требуют ручного управления памятью. Это источник утечек, висячих указателей и двойного освобождения. RAII-подход решает это.
Четыре инструмента
Node* — сырой указатель
Просто адрес. Никакого управления памятью. Требует ручного delete,
легко забыть, легко ошибиться. Оставлен в языке для совместимости и специальных задач.
unique_ptr<T>
Единственный владелец объекта. Нельзя копировать, можно только перемещать.
Освобождает память автоматически, когда выходит из области видимости.
Замена new/delete в 90% случаев.
shared_ptr<T>
Общий владелец: несколько указателей разделяют один объект. Внутренний счётчик ссылок удаляет объект, когда последний владелец уходит. Копируется свободно.
weak_ptr<T>
Наблюдатель. Не владеет объектом и не увеличивает счётчик. Используется, чтобы
«посмотреть» на объект, не создавая циклических ссылок. Перед доступом нужно
преобразовать в shared_ptr.
unique_ptr: единственный владелец
Основной инструмент в современном C++. Применяется везде, где раньше вы бы написали
new и delete.
#include <memory>
using namespace std;
// Создание (предпочтительно — через make_unique)
unique_ptr<int> p = make_unique<int>(42);
cout << *p << endl; // 42
// Массив
auto arr = make_unique<int[]>(10);
arr[0] = 100;
// НЕЛЬЗЯ копировать:
// unique_ptr<int> q = p; // ошибка компиляции
// МОЖНО переместить — владение переходит:
unique_ptr<int> q = move(p);
// теперь p == nullptr, а q владеет объектом
// Память освобождается автоматически
// при выходе q из области видимости
Что уникального:
- Нулевые накладные расходы по сравнению с сырым указателем: тот же размер, та же скорость.
- Гарантирует, что объект будет удалён ровно один раз.
- Компилятор не даст случайно скопировать указатель — это ошибка на этапе компиляции, а не баг во время работы.
unique_ptr. Не задумывайтесь, просто используйте его.
Жизненный цикл unique_ptr
shared_ptr: общий владелец
Когда объект должен использоваться сразу несколькими частями программы и никто не может сказать, кто владеет им «главный».
auto p1 = make_shared<int>(42);
cout << p1.use_count() << endl; // 1
{
auto p2 = p1; // копирование — оба владеют
cout << p1.use_count() << endl; // 2
auto p3 = p1;
cout << p1.use_count() << endl; // 3
} // p2 и p3 ушли — счётчик уменьшился
cout << p1.use_count() << endl; // 1
// когда p1 уйдёт — счётчик станет 0 и объект удалится
Интерактивно: счётчик ссылок
Добавляйте и удаляйте владельцев. Когда счётчик опустится до нуля — объект уничтожается.
shared_ptr друг на
друга — счётчики никогда не обнулятся, и объекты никогда не будут удалены. Утечка памяти.
Решение — сделать одну из ссылок weak_ptr.
weak_ptr: наблюдатель без владения
auto p = make_shared<int>(42);
weak_ptr<int> w = p; // w — наблюдатель
// weak_ptr НЕ увеличивает счётчик:
cout << p.use_count() << endl; // 1
// Чтобы обратиться к объекту, нужен lock():
if (auto sp = w.lock()) { // sp — shared_ptr, временный владелец
cout << *sp << endl; // 42
} else {
cout << "Объект уже удалён" << endl;
}
p.reset(); // объект удалён
if (w.expired()) { // weak_ptr умеет это проверять
cout << "Объект действительно удалён" << endl;
}
shared_ptr, дети смотрят на родителя через weak_ptr. Цикла нет —
утечки нет.
Что выбрать: шпаргалка
| Ситуация | Что использовать |
|---|---|
| Объект живёт в одной функции / одном месте программы | unique_ptr |
| Объект создаётся в фабрике и передаётся вызывающему | unique_ptr (возврат по значению) |
| Объектом владеют несколько независимых частей программы | shared_ptr |
| Наблюдение за объектом без владения | weak_ptr |
| Родитель-владелец и дети-наблюдатели | shared_ptr + weak_ptr |
| Функция не владеет объектом, только использует | Сырой указатель или ссылка (без владения) |
new и
delete вручную. Если нужен объект в куче — используйте make_unique
или make_shared. Если владение передаётся — unique_ptr.
Если владение разделяется — shared_ptr. Сырые указатели оставьте для тех
редких случаев, когда функция лишь наблюдает объект, не владея им.
Задание
- Создайте
unique_ptr<int>, выведите значение, обнулите черезreset(). Проверьте, что указатель стал нулевым. - Попробуйте скопировать
unique_ptr— что говорит компилятор? А если использоватьstd::move? - Создайте
shared_ptrи два его копии. Выведитеuse_count()на каждом шаге. Обнулите копии по одной — как меняется счётчик? - Напишите класс
Node, содержащийshared_ptr<Node>на следующий узел. Создайте цепочку из 3 узлов и разорвите её. Что произошло со счётчиками? - Переделайте задачу про
swapтак, чтобы она работала не с сырыми указателями, а сunique_ptr. Можно ли поменять два объекта местами? - Объясните: почему
make_sharedпредпочтительнее, чемshared_ptr<T>(new T)? Подсказка: количество выделений памяти.