Avatar ·

Что такое умные указатели (smart pointers) и когда их нужно использовать?

Привет, коллеги! Я пишу на C++ не так давно, но уже столкнулся с проблемой утечек памяти. Я пока использую обычные «сырые» указатели (raw pointers) с `new` и `delete`, но постоянно забываю вызвать `delete`, особенно когда код обрастает исключениями и ранними возвратами. Например: ```cpp void process() { Resource* res = new Resource(); // ... какой-то код, который может выбросить исключение ... delete res; // если было исключение, сюда не дойдём — утечка! } ``` Я слышал про «умные указатели» (smart pointers) в C++11, но не до конца понимаю, что это такое. В интернете много информации, но хочется чёткого объяснения: что это за звери, чем они отличаются от обычных указателей, и в каких ситуациях их реально стоит применять? Особенно интересуют `std::unique_ptr`, `std::shared_ptr` и `std::weak_ptr`. Когда какой выбирать? Есть ли подводные камни? Буду благодарен за примеры кода и пояснения для новичков. <<>>

Что такое умный указатель?

Умный указатель (smart pointer) — это класс-обёртка над обычным указателем, который автоматически управляет временем жизни объекта. Когда умный указатель уничтожается (выходит из области видимости, удаляется вручную и т.д.), он автоматически вызывает delete для своего ресурса. Это избавляет от ручного освобождения памяти и помогает избежать утечек, даже при исключениях.

В C++11 появились три основных типа умных указателей из заголовочного файла <memory>:

  • std::unique_ptr — единоличный владелец ресурса. Его нельзя копировать, только перемещать.
  • std::shared_ptr — разделяемое владение. Ресурс живёт, пока существует хотя бы один shared_ptr, ссылающийся на него (используется подсчёт ссылок).
  • std::weak_ptr — «слабый» наблюдатель за shared_ptr. Не увеличивает счётчик ссылок, нужен для разрыва циклических зависимостей.

Когда использовать каждый тип?

1. std::unique_ptr

Используйте std::unique_ptr всегда, когда у объекта должен быть один чёткий владелец. Это наиболее эффективный умный указатель (нулевые накладные расходы по сравнению с raw pointer). Он идеален для:

  • Владения ресурсом внутри класса (как член класса).
  • Возврата динамически созданного объекта из функции.
  • Хранения объектов в контейнерах, где каждый элемент принадлежит контейнеру.
#include <memory>

std::unique_ptr<Resource> createResource() {
    return std::make_unique<Resource>(); // C++14
}

void useResource() {
    auto ptr = createResource();
    // ptr — единственный владелец
    // при выходе из области видимости память освободится
} // автоматический delete

2. std::shared_ptr

Используйте std::shared_ptr, когда ресурс может быть разделён между несколькими владельцами, и вы не знаете, кто из них «умрёт» последним. Примеры:

  • Графовые структуры, где на узел могут ссылаться несколько других узлов.
  • Кэширование или пулы ресурсов.
  • Объекты, время жизни которых должно определяться последним пользователем.
std::shared_ptr<Resource> sp1 = std::make_shared<Resource>();
{
    std::shared_ptr<Resource> sp2 = sp1; // теперь счётчик ссылок = 2
    // оба указателя работают с одним объектом
} // sp2 уничтожен, счётчик = 1
// sp1 уничтожен, счётчик = 0 — объект удалён

Важно: shared_ptr дороже unique_ptr (нужен счётчик ссылок + потокобезопасность). Используйте его только когда действительно нужно разделяемое владение.

3. std::weak_ptr

std::weak_ptr — это «наблюдатель» за shared_ptr. Он не увеличивает счётчик ссылок, поэтому не продлевает жизнь объекту. Основное применение — разрыв циклических ссылок. Если у вас есть два объекта, которые ссылаются друг на друга через shared_ptr, они никогда не удалятся (цикл). Один из них должен использовать weak_ptr.

class Node {
    std::shared_ptr<Node> next; // может создать цикл
    std::weak_ptr<Node> parent; // безопасно — слабая ссылка
};

// Чтобы получить доступ к объекту через weak_ptr, нужно вызвать lock()
std::weak_ptr<Resource> wp = sp1;
if (auto locked = wp.lock()) { // если объект ещё жив
    locked->doSomething();
} else {
    // объект уже удалён
}

Когда не нужны умные указатели?

  • Если объект создаётся на стеке (локальная переменная) — используйте обычную переменную, не указатель.
  • Если вы работаете с чужим API, который требует raw pointer (например, передача «this» или legacy-код) — используйте .get() для получения сырого указателя, но не удаляйте его вручную.
  • Для невладеющих наблюдений (если вы точно знаете, что объект живёт дольше) — можно использовать обычный указатель, но это уже зона ответственности программиста.

Золотое правило: по умолчанию используйте std::unique_ptr. Если нужно разделяемое владение — std::shared_ptr. Если есть риск циклических ссылок — std::weak_ptr. И никогда не смешивайте умные и сырые указатели для владения одним ресурсом.

<<>>

Дополнение: std::make_unique и std::make_shared

Всегда старайтесь использовать функции std::make_unique (C++14) и std::make_shared вместо прямого вызова new. Причины:

  • Безопасность при исключениях. Если вы пишете f(std::shared_ptr<T>(new T), otherFunction()), компилятор может выполнить new T, потом otherFunction(), и если последняя выбросит исключение — память утечёт. std::make_shared этого не допускает.
  • Производительность. std::make_shared выделяет память под объект и счётчик ссылок одним блоком, что уменьшает количество аллокаций.
  • Читаемость кода. Не нужно писать new и тип дважды.
// Плохо:
std::shared_ptr<Resource> sp(new Resource());

// Хорошо (C++14):
std::unique_ptr<Resource> up = std::make_unique<Resource>();
std::shared_ptr<Resource> sp = std::make_shared<Resource>();

Единственное исключение — когда вам нужен пользовательский deleter (например, для закрытия файла или освобождения ресурса из C-библиотеки). В этом случае make_* не подходит, и нужно использовать конструктор с deleter'ом.