Умные указатели: современный подход

Проблема «сырых» указателей с new/delete в том, что программист обязан не забыть освободить память. Умные указатели делают это автоматически — и устраняют целый класс ошибок.

unique_ptr shared_ptr weak_ptr RAII

Зачем нужны умные указатели

«Сырые» указатели требуют ручного управления памятью. Это источник утечек, висячих указателей и двойного освобождения. RAII-подход решает это.

Принцип RAII. Resource Acquisition Is Initialization — «получение ресурса есть инициализация». Идея: каждый ресурс получается в конструкторе объекта и освобождается в его деструкторе. Тогда, что бы ни случилось (включая исключения), ресурс будет освобождён автоматически.

Четыре инструмента

устаревшее

Node* — сырой указатель

Просто адрес. Никакого управления памятью. Требует ручного delete, легко забыть, легко ошибиться. Оставлен в языке для совместимости и специальных задач.

C++11

unique_ptr<T>

Единственный владелец объекта. Нельзя копировать, можно только перемещать. Освобождает память автоматически, когда выходит из области видимости. Замена new/delete в 90% случаев.

C++11

shared_ptr<T>

Общий владелец: несколько указателей разделяют один объект. Внутренний счётчик ссылок удаляет объект, когда последний владелец уходит. Копируется свободно.

C++11

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

auto p = make_unique<int>(42);
cout << *p; // используем
auto q = move(p); // передаём владение
// q выходит из области видимости → delete
Готово к запуску
$ // нажмите «Шаг»

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 и объект удалится

Интерактивно: счётчик ссылок

Добавляйте и удаляйте владельцев. Когда счётчик опустится до нуля — объект уничтожается.

Счётчик: 1
$ shared_ptr создан, use_count() = 1
Циклические ссылки. Если два объекта содержат 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
Функция не владеет объектом, только использует Сырой указатель или ссылка (без владения)
Главное правило современного C++. Не пишите new и delete вручную. Если нужен объект в куче — используйте make_unique или make_shared. Если владение передаётся — unique_ptr. Если владение разделяется — shared_ptr. Сырые указатели оставьте для тех редких случаев, когда функция лишь наблюдает объект, не владея им.

Задание

  1. Создайте unique_ptr<int>, выведите значение, обнулите через reset(). Проверьте, что указатель стал нулевым.
  2. Попробуйте скопировать unique_ptr — что говорит компилятор? А если использовать std::move?
  3. Создайте shared_ptr и два его копии. Выведите use_count() на каждом шаге. Обнулите копии по одной — как меняется счётчик?
  4. Напишите класс Node, содержащий shared_ptr<Node> на следующий узел. Создайте цепочку из 3 узлов и разорвите её. Что произошло со счётчиками?
  5. Переделайте задачу про swap так, чтобы она работала не с сырыми указателями, а с unique_ptr. Можно ли поменять два объекта местами?
  6. Объясните: почему make_shared предпочтительнее, чем shared_ptr<T>(new T)? Подсказка: количество выделений памяти.