Владение как идея
Главный вопрос при работе с динамической памятью — кто отвечает за освобождение объекта и когда. Умные указатели из <memory> делают ответ явным в типе: unique_ptr — единоличное владение, shared_ptr — разделяемое со счётчиком ссылок, weak_ptr — наблюдение без владения.
Это не просто «удобные обёртки». Это инструмент документирования архитектуры: по сигнатуре функции сразу видно, забирает она объект себе или только смотрит.
#include <memory>
std::unique_ptr<Widget> makeWidget(); // фабрика отдаёт владение вам
void render(const Widget& w); // только смотрит — без умных указателей
unique_ptr — владение без накладных расходов
std::unique_ptr — обёртка над сырым указателем размером ровно с указатель. При уничтожении указателя удаляется и объект. Копировать нельзя, перемещать — можно: после std::move исходный указатель обнуляется.
auto p = std::make_unique<int>(42); // C++14
*p = 10;
auto q = std::move(p); // p теперь пустой, q владеет объектом
// p.get() == nullptr
make_unique предпочтительнее new: короче, исключение-безопасно и запрещает случайное двойное владение. Массивы тоже поддерживаются:
auto arr = std::make_unique<int[]>(100); // new int[100], удалит через delete[]
arr[5] = 7;
Передача в функции
f(std::unique_ptr<T> p)— функция забирает объект. Вызывающий обязан написатьstd::move.f(T& obj)илиf(T* p)— функция только использует, владение остаётся у вызывающего.
void consume(std::unique_ptr<int> p) { /* владеет */ }
void peek(const int& v) { /* не владеет */ }
auto p = std::make_unique<int>(5);
peek(*p);
consume(std::move(p)); // после этой строки p пуст
shared_ptr — разделяемое владение
shared_ptr хранит счётчик ссылок: каждая копия увеличивает счётчик, каждое уничтожение — уменьшает. Объект удаляется, когда счётчик доходит до нуля.
auto a = std::make_shared<std::string>("данные");
{
auto b = a; // счётчик = 2
auto c = a; // счётчик = 3
} // b, c уничтожены — счётчик = 1
// a уничтожается — счётчик = 0, строка удалена
Цена: блок управления (счётчики + deleter) выделяется в куче, а инкремент/декремент — атомарные операции. Поэтому shared_ptr применяют только когда владение реально разделяется. «На всякий случай» — плохая причина.
make_shared выделяет объект и блок управления одной аллокацией — быстрее и компактнее, чем shared_ptr<T>(new T).
weak_ptr — наблюдатель и разрыв циклов
Классическая проблема shared_ptr — циклические ссылки: объекты держат друг друга и никогда не удаляются. Утечка памяти в чистом виде.
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // назад — weak, разрываем цикл!
};
weak_ptr не увеличивает счётчик и не может разыменоваться напрямую. Сначала его «запирают» через lock() — получается временный shared_ptr или пустота, если объект уже умер:
std::weak_ptr<int> w;
{
auto s = std::make_shared<int>(7);
w = s;
} // s умер
if (auto locked = w.lock()) {
std::cout << *locked;
} else {
std::cout << "объект уничтожен\n"; // сюда попадём
}
Типичные сценарии: кэши, списки подписчиков, родительские ссылки в деревьях, наблюдатели.
Свой deleter
Умный указатель умеет вызывать не только delete. Кастомный удалитель нужен для C-ресурсов: файлов, сокетов, контекстов библиотек:
auto fileDeleter = [](FILE* f) { if (f) fclose(f); };
std::unique_ptr<FILE, decltype(fileDeleter)> fp(fopen("log.txt", "w"), fileDeleter);
У unique_ptr тип deleter — часть типа (может нести состояние). У shared_ptr deleter стирается по типу — удобнее, но чуть тяжелее.
Когда что брать — шпаргалка
- Один владелец (99 % случаев):
unique_ptr. - Объект живёт, пока хоть кто-то его держит (общий ресурс, асинхронные задачи):
shared_ptr+make_shared. - Наблюдатель, кэш, обратные ссылки:
weak_ptr. - Просто посмотреть/использовать внутри вызова:
T&илиT*— сырой указатель как невладеющий параметр это нормально. thisнаружу из shared-объекта: наследуйтеstd::enable_shared_from_thisи зовитеshared_from_this().
Частые ошибки
shared_ptr<T>(new T)вместоmake_shared— лишняя аллокация и исключение-опасно.- Два
shared_ptrна один сырой указатель — двойное удаление. - Хранить
weak_ptrбез проверкиlock()на каждое использование. unique_ptrв контейнере — нормально, но копировать контейнер нельзя, только перемещать.- Гонки: счётчик ссылок атомарен, но сам объект не потокобезопасен автоматически.
Итог
Формула современного C++: сырой new почти не пишут — unique_ptr по умолчанию, shared_ptr при разделении, weak_ptr против циклов. Компилятор сам следит за временем жизни, утечки становятся редкостью, а семантика владения читается прямо из кода.
Практический паттерн: фабрика + unique_ptr
class Transport {
public:
virtual void send(std::string_view data) = 0;
virtual ~Transport() = default;
};
std::unique_ptr<Transport> makeTransport(std::string_view kind) {
if (kind == "tcp") return std::make_unique<TcpTransport>();
if (kind == "udp") return std::make_unique<UdpTransport>();
throw std::invalid_argument("неизвестный транспорт");
}
Вызывающий получает владение уникальным указателем — семантика ясна из сигнатуры, утечки исключены.
unique_ptr в классах: pImpl
Умные указатели позволяют «указатель на реализацию» без утечек:
class Widget {
struct Impl; // только объявление в .h
std::unique_ptr<Impl> impl_;
public:
Widget();
~Widget(); // важно: в .cpp, где Impl полный
};
Приватность реализации, быстрая перекомпиляция при правках Impl, автоматическое владение.
shared_ptr: реальный сценарий
Разделяемое владение уместно, когда объект действительно принадлежит нескольким — например, соединение с БД, которое используют несколько сервисов и живёт, пока жив хоть один:
class DbPool {
std::vector<std::shared_ptr<Connection>> conns_;
public:
std::shared_ptr<Connection> acquire();
};
Заметьте: vector<shared_ptr> внутри пула + shared_ptr наружу — соединение живёт, пока хоть кто-то держит.
Проверка времени жизни: weak_ptr в кэше
std::unordered_map<std::string, std::weak_ptr<Texture>> cache;
std::shared_ptr<Texture> getTexture(const std::string& name) {
if (auto it = cache.find(name); it != cache.end()) {
if (auto tex = it->second.lock()) return tex; // ещё жива
}
auto tex = loadTexture(name);
cache[name] = tex;
return tex;
}
Кэш не продлевает жизнь текстур — когда последний владелец отпустил, память освобождается, а weak-связь в кэше просто «устаревает».
Производительность: измерьте прежде чем менять
shared_ptr дороже unique_ptr из-за блока управления и атомарных счётчиков. Но в 99 % кода разница невидима — не разменивайте семантику на микрооптимизацию без профиля. Берите unique_ptr по умолчанию, shared_ptr только когда владение реально разделяется — и вопрос производительности почти не встанет.