Skip to the content.

Умные указатели


Если мы что-то выделяем в динамической памяти, нам приходиться следить за утечками и соответственно освобождать её после того как использовали:

int * ptr = new Item;
// усердно работаем
delete ptr;

Однако в большой программе можно легко запутаться и забыть очистить память. Эту проблему призваны решить умные указатели, которые будут следить за выделенной им памятью.

Самый простой умный указатель

template<typename T>
class SmartPtr
{
public:
    explicit SmartPtr(T * ptr) : ptr(ptr) {}
    
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }

    ~SmartPtr() { delete ptr; }
    
private:
    T * ptr;
    
    SmartPtr(SmartPtr& other);
    SmartPtr operator=(SmartPtr& other);
};

Соответственно мы работаем с SmartPtr как с обычным указателем, однако при его удалении срабатывает деструктор, который освобождает выделенную память:

SmartPtr<int> ptr(new int(5));
std::cout << *ptr << "\n";

Конструктор копирования и оператор присваивания приватные, поскольку одной из проблем такого примитивного указателя является повторное освобождение памяти:

SmartPtr<int> ptr(new int(5));
SmartPtr<int> ptr2(ptr); 
// оба указателя указывают на одну и ту же область памяти
// вызывается деструктор ptr2
// вызывается деструктор ptr, однако память на которую он указывает уже была освобождена -> ошибка

Стандартная библиотека

Файл заголовков: #include <memory>

В стандартной библиотеке реализованы следующие умные указатели:

Все они по разному решают проблему повторного освобождения памяти:

auto_ptr при копировании\присваивании просто разрывает связь с областью памяти у старого объекта:

auto_ptr<int> ptr(new int(5));  // указывает на 5
auto_ptr<int> ptr2(ptr);        // ptr2 теперь указывает на 5, а ap больше нет
// соответственно теперь работать с данными может только ptr2

unique_ptr вообще запретил копирование\присваивание:

unique_ptr<int> up(new int(5));
unique_ptr<int> up2(up);         // запрещено

Однако если нам все же нужно передать значение - можно сымитировать поведение auto_ptr:

unique_ptr<int> up(new int(5));
unique_ptr<int> up2;

up2 = std::move(up); 
// либо up2.swap(up);

shared_ptr помимо указателя на данные содержит в себе указатель на счётчик (int * counter) общий для всех умных указателей на данную область памяти. Соответственно освобождение области памяти произойдёт только тогда, когда счётчик станет 0:

shared_ptr<int> sp(new int(5));
shared_ptr<int> sp2(sp);
Основные методы  
ptr.get(); Вытянуть сырой (raw) указатель. Умный указатель является оберткой над простым указателем (как в самописном умном указателе выше) и мы вытягиваем адрес, который лежит в этом указателе
ptr.reset(); Очистить область памяти на которую указывает умный указатель, соответственно сам указатель станет пустым
ptr.release(); Очистить сам указатель, однако не освобождать область памяти на которую он указывает
ptr.swap(ptr2) Обменять значения указателей

Некоторые фишки:

1. static_cast<bool>(ptr) - возвращает true, если указатель действительно владеет ресурсом

2. unique_ptr в отличии от auto_ptr способен работать с динамическими массивами (потому что может различить delete и delete []), однако все еще лучшим способом работать с ними является std::vector.

3. Начиная с С++14 добавили шаблонную функцию std::make_unique(), которая создаёт объект типа шаблонна в динамической памяти и инициализирует его аргументами переданными в эту функцию:

// создать Item, передав в конструктор 7 и 11
std::unique_ptr<Item> ptr = std::make_unique<Item>(7, 11);
// или
auto ptr = std::make_unique<Item>(7, 11);

Аналогичная функция существует для shared_ptr - std::make_shared():

auto ptr = std::make_shared<Item>();
auto ptr2 = ptr;

4. Правило: всегда выполняйте копирование существующего std::shared_ptr, если нужно создать более одного shared_ptr, поскольку может случиться такая беда:

// следующие shared указатели не знают о существовании друг друга,
// потому что int * counter в каждом умном указателе создался свой,
// а не скопировался с другого указателя
Item * item = new Item;

std::shared_ptr<Item> s1(item);
{
    std::shared_ptr<Item> s2(item);
} // s2 освободит Item
// ошибка: повторное освобождение

// правильно было бы так:
// std::shared_ptr<Item> s2(s1);

5. unique_ptr можно преобразовать в shared_ptr передав в конструктор последнего rvalue:

std::unique_ptr<int> p(new int(6));
std::shared_ptr<int> s(std::move(p));

Проблема циклической зависимости

Напишем класс Human, у каждого человека будет name и mate и функцию которая принимает двух людей и делает их mates:

class Human
{
public:
    Human(std::string name) : name(name)
    {
        std::cout << "Created " << name << "\n";
    }

friend void bindMates(std::shared_ptr<Human> h1,
                      std::shared_ptr<Human> h2);

    ~Human()
    {
        std::cout << "Destroyed " << name << "\n";
    }

private:
    std::string name;
    std::shared_ptr<Human> mate;
};

void bindMates(std::shared_ptr<Human> h1,
               std::shared_ptr<Human> h2)
{
    h1->mate = h2;
    h2->mate = h1;
    std::cout << h1->name << " got new mate: " << h1->mate->name << "\n";
    std::cout << h2->name << " got new mate: " << h2->mate->name << "\n";
}

Создадим двух людей и посмотрим на результат работы:

std::shared_ptr<Human> h1(new Human("Andrew"));
std::shared_ptr<Human> h2(new Human("Ivan"));

Все как и предполагалось:

Created Andrew
Created Ivan
Destroyed Ivan
Destroyed Andrew

Попробуем теперь их “подружить”: bindMates(h1, h2);

Динамическая память не освободилась и произошла утечка, совсем не то что мы ожидали увидеть:

Created Andrew
Created Ivan
Andrew got new mate: Ivan
Ivan got new mate: Andrew
// не освободилась память

Так происходит потому что shared_ptr когда выходит из области видимости проверяет свой счётчик, и освобождает область памяти только в случае если он последний указатель на эту область. Соответственно:

Таким образом у нас по завершению работы программы все еще остается два указателя: h1->mate и h2->mate, поэтому объекты Andrew и Ivan не уничтожаются.

cycle_shared

Для решения этой проблемы был создан еще один умный указатель - std::weak_ptr. Он работает только в паре с std::shared_ptr, он так же указывает на объект, может получить к нему доступ, однако он не считается владельцем. Т.е. при выходе из области std::shared_ptr проверяя счётчик не будет учитывать std::weak_ptr. Перепишем нашу программу:

class Human
{
public:
    Human(std::string name) : name(name)
    {
        std::cout << "Created " << name << "\n";
    }

friend void bindMates(std::shared_ptr<Human> h1,
                      std::shared_ptr<Human> h2);

    ~Human()
    {
        std::cout << "Destroyed " << name << "\n";
    }

private:
    std::string name;
    std::weak_ptr<Human> mate; // здесь теперь weak_ptr
};

void bindMates(std::shared_ptr<Human> h1,
               std::shared_ptr<Human> h2)
{
    h1->mate = h2;
    h2->mate = h1;
    std::cout << h1->name << " got new mate: " << h2->name << "\n";
    std::cout << h2->name << " got new mate: " << h1->name << "\n";
}

Теперь все работает как надо:

Created Andrew
Created Ivan
Andrew got new mate: Ivan
Ivan got new mate: Andrew
Destroyed Ivan
Destroyed Andrew

Теперь h1 выходя из области проверяет счётчик, не считая weak_ptr он последний кто указывал на эту область памяти, значит он её освобождает. Аналогично поступает h2.

Недостаток weak_ptr в том, что у него нет оператора ->, что бы его использовать нужно вначале конвертировать его в std::shared_ptr при помощи метода lock():

std::shared_ptr<Human> getMate() const
{
    return mate.lock();
}