Умные указатели
Если мы что-то выделяем в динамической памяти, нам приходиться следить за утечками и соответственно освобождать её после того как использовали:
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 - на самом деле устарел, а на его смену пришел unique_ptr
- unique_ptr - умнее чем auto_ptr и способен различать когда
delete
, а когдаdelete []
- shared_ptr
- weak_ptr - отличается от остальных трёх, работает только вместе с shared_ptr
Все они по разному решают проблему повторного освобождения памяти:
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 выходя из main проверяет счётчик и узнаёт, что h2->mate указывает на того же Human что ион, соответственно он ничего не освобождает
- h2 выходя из main проверяет счётчик и узнаёт, что h1->mate указывает на того же Human что и он, соответственно он тоже ничего не освобождает
Таким образом у нас по завершению работы программы все еще остается два указателя: h1->mate и h2->mate, поэтому объекты Andrew и Ivan не уничтожаются.
Для решения этой проблемы был создан еще один умный указатель - 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();
}