Об интеллектуальных указателях C++
Можно ли обойтись без использования оператора delete и при этом избежать утечек динамической памяти? Да. Сегодня мы рассмотрим механизм, позволяющий это сделать автоматически.
Известно, что при динамическом выделении памяти требуется выполнять её очистку после того как данные больше не используются. При использовании классических («сырых») указателей очистку требуется выполнять в ручном режиме, при помощи оператора delete. Зачастую из-за ошибки человеческого фактора это условие может быть невыполненным, а в большом проекте на поиск подобной ошибки может уйти большое количество времени.
На помощь приходят интеллектуальные указатели из стандартной библиотеки C++.
Рассматриваемые в статье указатели добавлены в стандарт языка C++ ISO/IEC 14882:2011 (или C++11), вышедший в 2011 году (ранее существовал интеллектуальный указатель std::auto_ptr, но в C++11 он признан устаревшим, а на данный момент и вовсе удалён из стандарта, начиная с C++17).
Основными интеллектуальными указателями на данный момент являются:
•std::unique_ptr<>;
•std::shared_ptr<>.
Для того чтобы воспользоваться этими классами, следует подключить часть стандартной библиотеки memory:
#include <memory>
std::unique_ptr
std::unique_ptr<> - обеспечивает монопольное владение объектом. Принцип заключается в том, что данный указатель нельзя копировать (это не позволит сделать компилятор). Очистка памяти происходит автоматически, как только данный указатель выходит за область видимости.
Пример объявления интеллектуального указателя:
std::unique_ptr<T> ptr;
Здесь в качестве типа T может выступать любой класс или примитивный тип, который мог бы использоваться в качестве обычного указателя (с семантикой T*). Например, интеллектуальный указатель на целое число может быть объявлен так:
std::unique_ptr<int> ptr;
Для создания объекта применяется функция std::make_unique<T> (T - тип создаваемого объекта), например:
ptr = std::make_unique<int>(5); //Записываем в динамическую память число 5
Для разыменования интеллектуального указателя применяется та же семантика, как и для классического - унарная операция *:
*ptr += 4; //увеличиваем записанное число на 4
Также, доступна операция - >, например:
std::unique_ptr<std::vector> vectorPtr = std::make_unique<std::vector<int> >();
auto size = vectorPtr- >size(); //Вызов операции - >
Копирование объектов такого указателя запрещено (поскольку он гарантирует монопольное владение объектом):
auto ptr = std::make_unique<int>(3);
auto ptr2 = ptr; //Ошибка! Копирование std::unique_ptr запрещено.
Если Вам нужно, чтобы копирование было доступно, то читайте раздел std::shared_ptr этой статьи.
При этом разрешается перемещать такие указатели:
auto ptr = std::make_unique<int>(3);
auto ptr2 = std::move(ptr); //передача объекта принадлежащего ptr указателю ptr2
//Внимание: теперь ptr не владеет объектом!
//bool ptrIsEmpty = (ptr == nullptr); // True
Отметим также, что nullptr - значение нулевого указателя. При инициализации любых указателей нулевым значением рекомендуется использовать именно его (по ряду причин), а не NULL или 0.
Ещё раз подчеркну, что операция delete не требуется: объект автоматически удалится из памяти при выходе указателя за область видимости.
Данный указатель не имеет дополнительных накладных расходов с точки зрения производительности по сравнению с обычным указателем, при этом гарантируя очистку памяти.
std::shared_ptr
std::shared_ptr<> обеспечивает множественное (совместное) владение объектом.
Объект удаляется из памяти тогда, когда на него не ссылается ни один экземпляр указателя (то есть когда все ссылки вышли за область видимости).
Пример объявления такого указателя:
std::shared_ptr<T> ptr;
Создание указателя осуществляется при помощи функции std::make_shared<T> (T - тип создаваемого объекта), например:
auto ptr = std::make_shared<int>(5); //Создаём объект целого числа, равного 5
Данный указатель допускается копировать:
auto ptr = std::make_shared<int>(5);
auto ptr2 = ptr; // Копирование указателя
Также, допускается перемещение:
auto ptr = std::make_shared<int>(3);
auto ptr2 = std::move(ptr);
При перемещении исходный указатель (здесь - ptr) перестаёт ссылаться на объект и становится равным nullptr, аналогично std::unique_ptr.
Особенностью данного указателя является то, что внутри он содержит счётчик ссылок. При копировании он увеличивается на 1, при выходе ссылки за область видимости - уменьшается на 1, при перемещении - не изменяется. Когда счётчик ссылок становится равным 0, объект автоматически удаляется из динамической памяти.
Для разыменования данного указателя также применяется операция *, а также доступна операция - > (см. примеры с std::unique_ptr выше -семантика аналогична).
Рассмотрим пример передачи интеллектуального указателя в качестве аргумента функции:
void foo(std::shared_ptr<int> ptr);
Указатель std::shared_ptr<T> можно получить путём перемещения в него объекта из монопольного указателя std::unique_ptr<T>, например:
std::unique_ptr<int> u_ptr = std::make_unique<int>(3);
std::shared_ptr<int> s_ptr = std::move(u_ptr); //теперь u_ptr == nullptr
В обратную сторону этот механизм не работает, то есть std::shared_ptr нельзя передать указателю с монопольным владением.
Указатель std::shared_ptr имеет немного большие вычислительные расходы по сравнению с обычными указателями и std::unique_ptr, так как он должен подсчитывать ссылки. Также, он затрачивает немного больше памяти, для хранения счётчика ссылок. Поэтому его рекомендуется применять только там, где недостаточно возможностей std::unique_ptr.
Применение std::shared_ptr там, где это необходимо, в общем случае оправдывает дополнительные расходы (по сути дела незначительные), поскольку гарантирует надёжность кода и отсутствие утечек памяти.
std::const_pointer_cast
Напоследок рассмотрим пример конвертирования интеллектуальных указателей.
Допустим, у нас имеется указатель на целое число:
std::shared_ptr<int> ptr = std::make_shared(5);
Допустим, мы хотим передать его другой функции, но при этом исключить возможность редактировать хранимое целое значение. То есть, получить std::shared_ptr<const int>. Это можно сделать так:
auto ptr2 = std::const_pointer_cast<const int>(ptr);
В данном примере ptr2 имеет тип std::shared_ptr<const int>, а ссылается на тот же объект, что и ptr.
Такое преобразование можно сделать неявно:
std::shared_ptr<int> ptr = std::make_shared(5);
std::shared_ptr<const int> ptr2 = ptr; //Неявное преобразование
Кроме того, доступны следующие преобразования:
•std::reinterpret_pointer_cast - позволяет изменить тип
•std::static_pointer_cast
•std::dynamic_pointer_cast
Подробнее про эти преобразования можно узнать тут.
