В C++ нет сборщика мусора, но есть правило, которое делает управление ресурсами почти автоматическим: RAII (Resource Acquisition Is Initialization). Ресурс захватывается в конструкторе объекта и освобождается в его деструкторе. Деструктор локального объекта вызывается при любом выходе из области видимости: обычном, через return, break или исключение.
void save(const std::string& path) {
std::ofstream out(path); // открыли файл
std::lock_guard lock(mutex_); // захватили мьютекс
out << data_;
} // закрыли файл и отпустили мьютекс автоматически
RAII работает для памяти, файлов, сокетов, мьютексов, транзакций. Объекты уничтожаются в порядке, обратном созданию: и локальные переменные, и члены класса (члены в порядке, обратном объявлению).
Почему не new и delete
Widget* w = new Widget;
process(w); // если тут исключение, delete не выполнится: утечка
delete w;
Ручное управление ломается на исключениях, ранних return и при путанице, кто владеет объектом. Правило современного C++: голые new и delete в прикладном коде не пишем. Владение выражаем типом.
std::unique_ptr: единоличное владение
auto p = std::make_unique<Widget>(42);
auto q = std::move(p); // владение передано, p теперь пустой
// auto r = q; // ошибка компиляции: копировать нельзя
- Владелец ровно один, копирование запрещено, перемещение разрешено.
- С обычным удалителем занимает столько же, сколько сырой указатель, и не добавляет накладных расходов.
- Есть версия для массивов
std::unique_ptr<T[]>(вызываетdelete[]) и пользовательские удалители:std::unique_ptr<FILE, decltype(&std::fclose)>. - Фабрики возвращают
unique_ptr, а функция, принимающая владение, берёт его по значению.
std::shared_ptr: совместное владение
auto a = std::make_shared<Widget>();
auto b = a; // счётчик ссылок 2
Устройство: сам shared_ptr хранит два указателя: на объект и на управляющий блок. В блоке счётчик сильных ссылок, счётчик слабых ссылок, удалитель и аллокатор. Объект уничтожается, когда сильный счётчик падает до нуля, блок освобождается, когда умрут и все weak_ptr.
std::make_sharedвыделяет объект и блок одним куском памяти: меньше аллокаций и лучше локальность. Минус: память объекта не вернётся, пока жив хоть одинweak_ptr.- Изменение счётчиков атомарно, поэтому копировать разные экземпляры
shared_ptrна один объект из разных потоков безопасно. Но доступ к самому объекту не синхронизирован, а одновременная запись в один и тот же экземплярshared_ptrэто гонка (для этого естьstd::atomic<std::shared_ptr<T>>в C++20). - Копирование
shared_ptrне бесплатно: атомарный инкремент. Передавайте его поconst&, если функция не забирает владение, а ещё лучше передавайтеT&.
std::weak_ptr: наблюдатель
weak_ptr не продлевает жизнь объекта. Чтобы воспользоваться объектом, его превращают в shared_ptr через lock(): если объект уже умер, вернётся пустой указатель.
Главное применение: разрыв циклов. Если родитель держит детей через shared_ptr, а дети родителя тоже через shared_ptr, счётчики никогда не упадут до нуля, и вся структура утечёт. Обратную ссылку делают weak_ptr (или сырым указателем, если время жизни гарантировано). Ещё применение: кеши и подписчики.
enable_shared_from_this
Если объекту, уже управляемому shared_ptr, нужно выдать указатель на себя, нельзя писать std::shared_ptr<T>(this): появится второй управляющий блок и двойное удаление. Наследуйтесь от std::enable_shared_from_this<T> и вызывайте shared_from_this().
Частые ошибки
deleteдля памяти изnew[]и наоборот: UB.- Два
shared_ptr, созданных из одного сырого указателя. - Циклы из
shared_ptrи «утечки» в долгоживущих кешах. shared_ptrвезде «на всякий случай»: владение размывается, растут накладные расходы. По умолчанию выбирайтеunique_ptr.- Полиморфное удаление через указатель на базу без виртуального деструктора.
Что спросят на собесе
- Что такое RAII и почему это главный механизм C++.
- Разница
unique_ptr,shared_ptr,weak_ptr, когда что выбрать. - Как устроен
shared_ptrи чемmake_sharedлучшеshared_ptr(new T). - Потокобезопасен ли
shared_ptr. - Как появляются циклы владения и как их разорвать.