Что такое умные указатели (smart pointers) и когда их нужно использовать?
Что такое умный указатель?
Умный указатель (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'ом.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги