Умные указатели в C++: что это и когда их использовать? Туториал

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему ваша программа "съела" всю память и как это исправить?

Вы когда-нибудь писали на C++ приложение, которое работало отлично ровно до тех пор, пока вы не закрыли окно? А потом обнаруживали, что оно "съело" гигабайт оперативной памяти и не вернуло ни байта? Или, что еще хуже, ваша программа внезапно падала с загадочным segmentation fault, а вы часами искали, где же забыли вызвать delete?

Если да — добро пожаловать в клуб. Ручное управление памятью в C++ — это как жонглирование бензопилами: вроде бы можно, но лучше доверить это профессионалам. Или, в нашем случае, — умным указателям (smart pointers).

Сегодня мы разберем, что это за звери, какие они бывают и, самое главное, когда какой из них использовать. Этот материал — адаптация знаменитого вопроса со Stack Overflow, набравшего тысячи просмотров. Мы сделаем его понятным, живым и с примерами. Поехали!

Что такое умный указатель? Простыми словами

Представьте себе обычный указатель (int* ptr = new int(5);). Это как ключ от кладовки с ресурсами. Вы открыли кладовку (выделили память), но если потеряете ключ или забудете закрыть дверь — ресурсы пропадут навсегда. Это и есть утечка памяти.

Умный указатель — это "ключ с автозакрыванием". Это класс-обертка, который хранит внутри себя обычный указатель и автоматически вызывает delete (или delete[]), когда объект больше не нужен. Когда умный указатель выходит из области видимости, его деструктор срабатывает и освобождает память. Магия? Нет, RAII (Resource Acquisition Is Initialization) — один из краеугольных камней C++.

Начиная с C++11, умные указатели стали частью стандартной библиотеки (<memory>). Забудьте про auto_ptr (он устарел и опасен). Теперь у нас есть три богатыря:

  • std::unique_ptr — единоличный владелец.
  • std::shared_ptr — ресурс с "коллективной собственностью".
  • std::weak_ptr — "наблюдатель", который не мешает жить.

Давайте разберем каждого на пальцах и с кодом.

std::unique_ptr: "Это мое, и ничье больше!"

std::unique_ptr — это самый простой и быстрый умный указатель. Он гарантирует, что владение объектом принадлежит только одному указателю. Вы не можете скопировать unique_ptr, но можете переместить его (передать владение).

Когда использовать? Почти всегда, когда вы создаете объект в куче и знаете, что у него будет один хозяин на всем протяжении жизни.

Пример: Создание и использование

#include <iostream>
#include <memory>  // для умных указателей

class Robot {
public:
    Robot(const std::string& name) : name_(name) {
        std::cout << "Робот " << name_ << " активирован!\n";
    }
    void speak() const { std::cout << name_ << ": Привет, мир!\n"; }
    ~Robot() { std::cout << "Робот " << name_ << " уничтожен.\n"; }
private:
    std::string name_;
};

int main() {
    // Создаем unique_ptr (никогда не используйте new!)
    std::unique_ptr<Robot> r1 = std::make_unique<Robot>("R2-D2");

    r1->speak();  // Работаем как с обычным указателем

    // std::unique_ptr<Robot> r2 = r1; // Ошибка! Копировать нельзя
    std::unique_ptr<Robot> r2 = std::move(r1); // Перемещаем владение

    if (!r1) {
        std::cout << "r1 теперь пуст, владение перешло к r2\n";
    }

    r2->speak();  // Работает
    // Здесь r2 выйдет из области видимости и робот будет уничтожен автоматически
    return 0;
}

Важно: Используйте std::make_unique<T>(args...) вместо new. Это безопаснее и исключает утечки при исключениях.

Когда использовать unique_ptr?

  • Владелец объекта очевиден и единственен.
  • Для хранения объектов в контейнерах (например, std::vector<std::unique_ptr<Shape>>).
  • Как возвращаемое значение из фабричных функций (компилятор оптимизирует копирование).
  • Для "легковесного" полиморфизма.

std::shared_ptr: "Вместе мы сила!"

А что, если на один и тот же объект нужно ссылаться из нескольких мест? Например, граф, где несколько узлов указывают на один ресурс. Или кэш. Тут приходит std::shared_ptr.

Он использует счетчик ссылок (reference counter). Каждый раз, когда вы копируете shared_ptr, счетчик увеличивается. Когда копия уничтожается — уменьшается. Когда счетчик становится равен нулю — объект удаляется.

Плата за удобство: немного больше памяти (под счетчик) и небольшое снижение производительности (атомарные операции).

Пример: Разделяем ресурс

#include <iostream>
#include <memory>
#include <vector>

class DataBase {
public:
    DataBase() { std::cout << "База данных открыта.\n"; }
    void query() { std::cout << "Выполняется запрос...\n"; }
    ~DataBase() { std::cout << "База данных закрыта.\n"; }
};

int main() {
    std::shared_ptr<DataBase> db1 = std::make_shared<DataBase>();
    {
        std::shared_ptr<DataBase> db2 = db1;  // Копируем, счетчик = 2
        std::vector<std::shared_ptr<DataBase>> clients;
        clients.push_back(db2);  // Счетчик = 3

        std::cout << "Счетчик ссылок: " << db1.use_count() << "\n";
        db1->query();
    } // db2 и вектор уничтожены, счетчик = 1

    std::cout << "Счетчик ссылок: " << db1.use_count() << "\n";
    // Выходим из main — db1 уничтожается, счетчик = 0, БД закрывается
    return 0;
}

Когда использовать shared_ptr?

  • Когда время жизни объекта должно определяться последним "владельцем".
  • В

Блоги

Рекомендаций по книгам