cpp

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

Классы могут быть очень большими или экземпляров класса может быть очень много, в результате мы вынуждены создавать объекты в динамической памяти, т. к. размер стека ограничен. В разд. 13.14 мы уже рассматривали способы динамического создания объектов с помощью оператора new. В этом разделе мы рассмотрим проблемы при работе с динамическими объектами, а также научимся эти проблемы решать.

Примечание

Итак, давайте создадим объект класса B внутри блока:

{
   B *pB = new B(20);
   // Выполняем операции
   std::cout << pB->y << std::endl; // 20
}
// Здесь указатель pB уже не существует
// Утечка памяти!!!

Указатель pB создан внутри блока (области, ограниченной фигурными скобками), следовательно, он является локальной переменной, область видимости которой ограничена блоком. После блока указатель pB уже не существует. В результате память под указатель будет освобождена, а вот динамическая память автоматически освобождена не будет и мы получим утечку памяти. Заметьте, что деструктор объекта не вызывается. Попробуйте вместо блока использовать бесконечный цикл while и понаблюдайте за размером памяти в Диспетчере задач Windows. Так вы очень быстро поймете, к чему может привести утечка памяти.

Чтобы избежать утечки памяти нужно динамическую память освобождать явным образом. Как вы уже знаете, для освобождения динамической памяти предназначен оператор delete. Давайте переделаем предыдущий пример и исправим ошибку:

{
   B *pB = new B(20);
   // Выполняем операции
   std::cout << pB->y << std::endl; // 20
   // Освобождаем динамическую память
   delete pB;
}
// Здесь указатель pB уже не существует
// Все равно возможна утечка памяти!!!

Итак, указатель после выхода из блока удаляется, динамическая память освобождается и вызывается деструктор. Вроде все хорошо, но это не так! Проблема заключается в том, что при выполнении операций с объектом может произойти ошибка, которая сгенерирует исключение. В результате поток управления не дойдет до оператора delete, динамическая память освобождена не будет и деструктор вызван не будет. В результате опять получаем утечку памяти:

try {
   B *pB = new B(20);
   // Выполняем операции
   throw std::exception();          // Генерация исключения
   std::cout << pB->y << std::endl;
   // Освобождаем динамическую память
   delete pB;
} catch (std::exception &ex) {
   std::cout << "Исключение обработано" << std::endl;
}
// Утечка памяти!!!

Результат (заметьте, что деструктор вызван не был):

B::B()
Исключение обработано

Для решения проблем с освобождением динамической памяти предназначены классы unique_ptr, shared_ptr и weak_ptr, которые называются «умными» указателями. Прежде чем использовать классы не забудьте подключить заголовочный файл memory:

#include <memory>

Класс unique_ptr: уникальный указатель

Шаблонный класс unique_ptr реализует уникальный указатель на объект. Такой указатель нельзя копировать, но можно передавать владение им. При удалении объекта указателя автоматически вызывается деструктор, внутри которого выполняется освобождение динамической памяти. Объявления класса unique_ptr:

template <typename _Tp, typename _Dp = default_delete<_Tp>>
    class unique_ptr;
template<typename _Tp, typename _Dp>
    class unique_ptr<_Tp[], _Dp>;

Управление одним объектом

Создать экземпляр класса unique_ptr можно следующими основными способами:

std::unique_ptr<B> ptr1;
std::unique_ptr<B> ptr2(nullptr);
std::unique_ptr<B> ptr3(new B(20));

После названия класса unique_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int. Первые две инструкции создают объекты с нулевым указателем, а последняя инструкция при создании сразу принимает право владения экземпляром класса B, созданном в динамической памяти. Объекты «умных» указателей создаются в локальной области видимости, поэтому при выходе из области видимости будет вызван деструктор «умного» указателя, внутри которого будет освобождена динамическая память и вызван деструктор объекта. Теперь даже при возникновении исключения динамическая память будет освобождена:

try {
   // #include <memory>
   std::unique_ptr<B> ptr(new B(20));
   // Выполняем операции
   throw std::exception();
   std::cout << ptr->y << std::endl;
} catch (std::exception &ex) {
   std::cout << "Исключение обработано" << std::endl;
}

Результат (деструктор был вызван):

B::B()
B::~B()
Исключение обработано

Если закомментировать инструкцию, генерирующую исключение, то результат будет таким (деструктор был вызван):

B::B()
20
B::~B()

Тут существует еще одна проблема. При динамическом создании объекта может возникнуть исключение еще до получения права владения. Чтобы этого избежать следует воспользоваться функцией make_unique(). Обратите внимание: функция доступна, начиная со стандарта C++14. Прототипы функции:

template<typename _Tp, typename... _Args>
   inline typename _MakeUniq<_Tp>::__single_object
   make_unique(_Args &&... args);
template<typename _Tp>
   inline typename _MakeUniq<_Tp>::__array
   make_unique(size_t num);

Пример:

std::unique_ptr<A> ptrA = std::make_unique<A>();
std::cout << ptrA->x << std::endl; // 0
std::unique_ptr<B> ptrB = std::make_unique<B>(20);
std::cout << ptrB->y << std::endl; // 20

При объявлении объекта удобно использовать ключевое слово auto:

auto ptrA = std::make_unique<A>();
std::cout << ptrA->x << std::endl; // 0

Класс unique_ptr перегружает операторы -> (доступ к члену) и * (разыменование), а также операторы сравнения:

auto ptrB = std::make_unique<B>(20);
std::cout << ptrB->y << std::endl;        // 20
std::cout << (*ptrB).y << std::endl;      // 20
auto ptr = std::make_unique<int>(30);
std::cout << *ptr << std::endl;           // 30
std::unique_ptr<B> p;
std::cout << (p == nullptr) << std::endl; // 1
if (!p) std::cout << "null" << std::endl; // null

Класс unique_ptr реализует уникальный указатель на объект. Такой указатель нельзя копировать, но можно передавать владение им другому «умному» указателю с помощью оператора присваивания и функции move():

std::unique_ptr<B> ptr1;
std::unique_ptr<B> ptr2 = std::make_unique<B>(20);
// Нельзя создавать копию
// ptr1 = ptr2;            // Ошибка
// Можно перемещать
ptr1 = std::move(ptr2);    // OK
std::cout << ptr1->y << std::endl;           // 20
std::cout << (ptr2 == nullptr) << std::endl; // 1

Перечислим основные методы класса unique_ptr:

  • get() — возвращает обычный указатель или нулевой указатель, при этом сохраняя право на владение объектом:
auto ptr = std::make_unique<B>(20);
B *pB = ptr.get();
std::cout << pB->y << std::endl; // 20
  • release() — возвращает обычный указатель или нулевой указатель, при этом передавая право на владение объектом. За освобождение динамической памяти «умный» указатель больше не отвечает, поэтому нужно выполнить освобождение самостоятельно:
auto ptr = std::make_unique<B>(20);
B *pB = ptr.release();
std::cout << pB->y << std::endl;            // 20
std::cout << (ptr == nullptr) << std::endl; // 1
// Нужно освободить память
delete pB;
  • swap() — меняет местами значения двух объектов:
auto ptr1 = std::make_unique<B>(10);
auto ptr2 = std::make_unique<B>(20);
ptr1.swap(ptr2);
std::cout << ptr1->y << std::endl; // 20
std::cout << ptr2->y << std::endl; // 10

Вместо метода swap() можно воспользоваться функцией swap():

auto ptr1 = std::make_unique<B>(10);
auto ptr2 = std::make_unique<B>(20);
std::swap(ptr1, ptr2);
std::cout << ptr1->y << std::endl; // 20
std::cout << ptr2->y << std::endl; // 10
  • reset() — освобождает динамическую память и обнуляет указатель:
auto ptr = std::make_unique<B>(20);
ptr.reset();
std::cout << (ptr == nullptr) << std::endl; // 1

Управление массивом

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

std::unique_ptr<A[]> ptr(new A[2]);
ptr[0].x = 10;
ptr[1].x = 20;
std::cout << ptr[0].x << std::endl; // 10
std::cout << ptr[1].x << std::endl; // 20

Пример создания динамического массива с помощью функции make_unique():

auto ptr = std::make_unique<A[]>(2); // 2 — это количество элементов
ptr[0].x = 10;
ptr[1].x = 20;
std::cout << ptr[0].x << std::endl; // 10
std::cout << ptr[1].x << std::endl; // 20

Как видно из примера, для доступа к элементу массива используются квадратные скобки, внутри которых указывается индекс элемента внутри массива.

Класс shared_ptr: совместно используемый указатель

Шаблонный класс shared_ptr реализует совместно используемый указатель. Такой указатель можно и копировать, и перемещать. Количество копий отслеживается с помощью счетчика. Когда счетчик становится равным нулю, выполняется освобождение динамической памяти. Объявление класса shared_ptr:

template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>;

Управление одним объектом

Создать экземпляр класса shared_ptr можно следующими основными способами (полный список смотрите в документации):

// #include <memory>
std::shared_ptr<B> ptr1;
std::shared_ptr<B> ptr2(nullptr);
std::shared_ptr<B> ptr3(new B(20));
std::shared_ptr<B> ptr4(ptr3);
std::unique_ptr<B> uptr(new B(5));
std::shared_ptr<B> ptr5(std::move(uptr));
std::cout << ptr1.use_count() << std::endl;  // 0
std::cout << ptr2.use_count() << std::endl;  // 0
std::cout << ptr3.use_count() << std::endl;  // 2
std::cout << ptr4.use_count() << std::endl;  // 2
std::cout << ptr5.use_count() << std::endl;  // 1
std::cout << (uptr == nullptr) << std::endl; // 1

После названия класса shared_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int. Первые две инструкции создают объекты с нулевым указателем, а третья инструкция при создании сразу принимает право владения экземпляром класса B, созданном в динамической памяти. Четвертая инструкция создает копию «умного» указателя. В результате счетчик ссылок у обоих указателей будет равен 2. Получить значение счетчика позволяет метод use_count(). В пятой инструкции создается указатель класса unique_ptr, а в шестой инструкции выполняется передача владения объектом указателю класса shared_ptr. При этом объект uptr будет хранить нулевой указатель.

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

{
   std::shared_ptr<B> ptr1(new B(20));            // B::B()
   std::cout << ptr1.use_count() << std::endl;    // 1
   {
      std::shared_ptr<B> ptr2(ptr1);
      std::cout << ptr1.use_count() << std::endl; // 2
      std::cout << ptr2.use_count() << std::endl; // 2
   }
   std::cout << ptr1.use_count() << std::endl;    // 1
}                                                 // B::~B()
// Динамическая память освобождается

При динамическом создании объекта может возникнуть исключение еще до получения права владения. Чтобы этого избежать следует воспользоваться функцией make_shared(). Прототип функции:

template<typename _Tp, typename... Args>
   inline shared_ptr<_Tp>
   make_shared(Args &&... __args)

Пример:

std::shared_ptr<A> ptrA = std::make_shared<A>();
std::cout << ptrA->x << std::endl; // 0
auto ptrB = std::make_shared<B>(20);
std::cout << ptrB->y << std::endl; // 20

Класс shared_ptr перегружает операторы -> (доступ к члену) и * (разыменование), а также операторы сравнения:

auto ptrB = std::make_shared<B>(20);         // B::B()
std::cout << ptrB->y << std::endl;           // 20
std::cout << (*ptrB).y << std::endl;         // 20
auto ptr = std::make_shared<int>(30);
std::cout << *ptr << std::endl;              // 30
ptrB.reset(); // Очистка (B::~B())
if (!ptrB) std::cout << "null" << std::endl; // null

Кроме того, выполнена перегрузка оператора <<, позволяющего вывести результат выполнения метода get() на консоль:

auto ptr = std::make_shared<int>(10);
std::cout << ptr << std::endl;   // Адрес (например, 0x586e60)
std::cout << *ptr << std::endl;  // Значение (10)

Класс shared_ptr реализует совместно используемый указатель. Такой указатель можно и копировать, и перемещать:

std::shared_ptr<B> ptr1;
std::shared_ptr<B> ptr2;
std::shared_ptr<B> ptr3 = std::make_shared<B>(20);
// Можно создать копию
ptr1 = ptr3;
std::cout << ptr1.use_count() << std::endl; // 2
// Можно перемещать
ptr2 = std::move(ptr1);
std::cout << ptr2.use_count() << std::endl; // 2
std::cout << ptr1.use_count() << std::endl; // 0
// Можно перемещать из unique_ptr
std::unique_ptr<B> uptr(new B(5));
ptr1 = std::move(uptr);
std::cout << ptr1.use_count() << std::endl; // 1

Перечислим основные методы класса shared_ptr:

  • use_count() — возвращает количество объектов, совместно использующих указатель (возвращает значение счетчика):
auto ptr = std::make_shared<B>(20);
std::cout << ptr.use_count() << std::endl; // 1
  • unique() — возвращает значение true, если значение счетчика равно 1, и false — в противном случае:
auto ptr = std::make_shared<B>(20);
if (ptr.unique())
   std::cout << "unique" << std::endl;     // unique
else std::cout << "no" << std::endl;
  • get() — возвращает обычный указатель (или нулевой указатель), при этом сохраняя право на владение объектом:
auto ptr = std::make_shared<B>(20);
B *pB = ptr.get();
std::cout << pB->y << std::endl;           // 20
std::cout << ptr.use_count() << std::endl; // 1
  • swap() — меняет местами значения двух объектов:
auto ptr1 = std::make_shared<B>(10);
auto ptr2 = std::make_shared<B>(20);
ptr1.swap(ptr2);
std::cout << ptr1->y << std::endl; // 20
std::cout << ptr2->y << std::endl; // 10

Вместо метода swap() можно воспользоваться функцией swap():

auto ptr1 = std::make_shared<B>(10);
auto ptr2 = std::make_shared<B>(20);
std::swap(ptr1, ptr2);
std::cout << ptr1->y << std::endl; // 20
std::cout << ptr2->y << std::endl; // 10
  • reset() — уменьшает значение счетчика на 1 и обнуляет указатель в текущем объекте. Если значение счетчика будет равно 0, то освобождает динамическую память. Если в качестве параметра передан указатель, то «умный» указатель получает право собственности на него:
auto ptr1 = std::make_shared<B>(20);        // B::B()
ptr1.reset();                               // B::~B()
std::cout << ptr1.use_count() << std::endl; // 0
auto ptr2 = std::make_shared<B>(30);        // B::B()
B *pB = new B(40);                          // B::B()
ptr2.reset(pB);                             // B::~B()
std::cout << ptr2.use_count() << std::endl; // 1
std::cout << ptr2->y << std::endl;          // 40
// B::~B()

Для приведения типов можно воспользоваться следующими функциями:

  • static_pointer_cast() — стандартное приведение типов (static_cast). Прототип функции:
template<typename _Tp, typename _Up>
   inline shared_ptr<_Tp>
   static_pointer_cast(const shared_ptr<_Up> &sp) noexcept;
  • dynamic_pointer_cast() — выполняет приведение типов указателей или ссылок (dynamic_cast). Применяется для приведения полиморфных типов. Прототип функции:
template<typename _Tp, typename _Up>
   inline shared_ptr<_Tp>
   dynamic_pointer_cast(const shared_ptr<_Up> &sp) noexcept;
  • const_pointer_cast() — приведение const_cast. Прототип функции:
template<typename _Tp, typename _Up>
   inline shared_ptr<_Tp>
   const_pointer_cast(const shared_ptr<_Up> &sp) noexcept;
  • reinterpret_pointer_cast() — приведение reinterpret_cast. Функция доступна, начиная со стандарта C++17. Прототип функции:
template<typename _Tp, typename _Up>
   inline shared_ptr<_Tp>
   reinterpret_pointer_cast(const shared_ptr<_Up> &sp) noexcept;

Управление массивом

Начиная со стандарта C++17, класс shared_ptr можно также использовать для работы с динамическими массивами. Чтобы вызывался оператор delete для массива нужно внутри угловых скобок после типа указать квадратные скобки:

std::shared_ptr<A[]> ptr(new A[2]);
ptr[0].x = 10;
ptr[1].x = 20;
std::cout << ptr[0].x << std::endl; // 10
std::cout << ptr[1].x << std::endl; // 20

Как видно из примера, для доступа к элементу массива используются квадратные скобки, внутри которых указывается индекс элемента внутри массива.

Класс weak_ptr: «слабый» указатель

При использовании класса shared_ptr можно получить циклическую зависимость, если первый объект ссылается на второй объект, а второй на первый, или объект ссылается сам на себя. Счетчик в данном случае никогда не станет равным нулю, а, следовательно, не будет освобождена динамическая память. Давайте рассмотрим пример. Предположим, у нас есть класс C:

class C {
public:
   std::shared_ptr<C> ptr;
   C() { std::cout << "C::C()" << std::endl; }
   ~C() { std::cout << "C::~C()" << std::endl; }
};

Если мы выполним следующие инструкции, то получим циклическую зависимость:

auto p = std::make_shared<C>();
p->ptr = p;
// Циклическая зависимость!!! Утечка памяти!!!

Решением проблемы являются «слабые» указатели, реализуемые с помощью класса weak_ptr. Достаточно в классе C объявить поле ptr с типом weak_ptr:

class C {
public:
   std::weak_ptr<C> ptr;
   C() { std::cout << "C::C()" << std::endl; }
   ~C() { std::cout << "C::~C()" << std::endl; }
};

Класс weak_ptr владельцем указателя не считается, поэтому при использовании следующих инструкций никакой циклической зависимости не будет:

auto p = std::make_shared<C>();
p->ptr = p; // OK

Объявление класса weak_ptr:

template<typename _Tp>
   class weak_ptr : public __weak_ptr<_Tp>;

Создать экземпляр класса weak_ptr можно следующими основными способами (полный список смотрите в документации):

// #include <memory>
std::weak_ptr<B> ptr1;
std::weak_ptr<B> ptr2(ptr1);
std::shared_ptr<B> sptr(new B(20));
std::weak_ptr<B> ptr3(sptr);
std::weak_ptr<B> ptr4(std::move(ptr3));
std::cout << ptr1.use_count() << std::endl;  // 0
std::cout << ptr2.use_count() << std::endl;  // 0
std::cout << ptr3.use_count() << std::endl;  // 0
std::cout << ptr4.use_count() << std::endl;  // 1
std::cout << sptr.use_count() << std::endl;  // 1

После названия класса weak_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int. Первая инструкция создает объект с нулевым указателем, а вторая инструкция создает копию объекта. В третьей инструкции создается объект класса shared_ptr, а в четвертой инструкции на его основе создается объект класса weak_ptr. В пятой инструкции передаются права на владение объектом, при этом объект ptr3 будет хранить нулевой указатель.

Объект класса weak_ptr является оболочкой над объектом класса shared_ptr. Правом владения указателя и ответственным за освобождение динамической памяти является объект класса shared_ptr. Использовать объект напрямую нельзя, т. к. класс weak_ptr не перегружает операторы -> и *, а также не содержит метода get(). Чтобы получить доступ, нужно выполнить преобразование в объект класса shared_ptr. Сделать это можно двумя способами:

  • передать объект класса weak_ptr конструктору класса shared_ptr. Если объект класса weak_ptr пустой, то генерируется исключение. Пример:
auto sptr = std::make_shared<B>(20);
std::weak_ptr<B> ptr(sptr);
std::shared_ptr<B> sp(ptr);
std::cout << sp->y << std::endl;  // 20
  • с помощью метода lock(). Если объект класса weak_ptr пустой, то возвращается пустой объект класса shared_ptr. Пример:
auto sptr = std::make_shared<B>(20);
std::weak_ptr<B> ptr(sptr);
std::shared_ptr<B> sp;
sp = ptr.lock();
if (sp) std::cout << sp->y << std::endl;  // 20

С помощью оператора = можно создать копию объекта класса weak_ptr, а также присвоить объект класса shared_ptr. Начиная со стандарта C++14, существует возможность перемещения:

auto sptr1 = std::make_shared<B>(20);
auto sptr2 = std::make_shared<B>(10);
std::weak_ptr<B> wptr1(sptr1);
std::weak_ptr<B> wptr2;
// Можно создать копию
wptr2 = wptr1;
std::shared_ptr<B> sp;
sp = wptr2.lock();
if (sp) std::cout << sp->y << std::endl;     // 20
// Можно присвоить объект класса shared_ptr
wptr2 = sptr2;
sp = wptr2.lock();
if (sp) std::cout << sp->y << std::endl;     // 10
// Можно перемещать
wptr1 = std::move(wptr2);
std::cout << wptr1.use_count() << std::endl; // 2
std::cout << wptr2.use_count() << std::endl; // 0

Класс weak_ptr содержит следующие основные методы:

  • use_count() — возвращает количество объектов, совместно использующих указатель (возвращает значение счетчика):
auto ptr = std::make_shared<B>(20);
std::weak_ptr<B> wptr(ptr);
std::cout << wptr.use_count() << std::endl; // 1
  • expired() — возвращает значение true, если значение счетчика равно 0, и false — в противном случае:
std::weak_ptr<B> wptr;
std::cout << std::boolalpha;
std::cout << wptr.expired() << std::endl;   // true
auto ptr = std::make_shared<B>(20);
wptr = ptr;
std::cout << wptr.expired() << std::endl;   // false
  • lock() — возвращает объект класса shared_ptr. Если объект класса weak_ptr пустой, то возвращается пустой объект класса shared_ptr:
auto sptr = std::make_shared<B>(20);
std::weak_ptr<B> ptr(sptr);
std::shared_ptr<B> sp;
sp = ptr.lock();
if (sp) std::cout << sp->y << std::endl;    // 20
  • swap() — меняет местами значения двух объектов:
auto sptr1 = std::make_shared<B>(10);
auto sptr2 = std::make_shared<B>(20);
std::weak_ptr<B> wptr1(sptr1);
std::weak_ptr<B> wptr2(sptr2);
wptr1.swap(wptr2);
std::cout << wptr1.lock()->y << std::endl;  // 20
std::cout << wptr2.lock()->y << std::endl;  // 10

Вместо метода swap() можно воспользоваться функцией swap():

auto sptr1 = std::make_shared<B>(10);
auto sptr2 = std::make_shared<B>(20);
std::weak_ptr<B> wptr1(sptr1);
std::weak_ptr<B> wptr2(sptr2);
std::swap(wptr1, wptr2);
std::cout << wptr1.lock()->y << std::endl;  // 20
std::cout << wptr2.lock()->y << std::endl;  // 10
  • reset() — делает объект пустым:
auto sptr = std::make_shared<B>(10);
std::weak_ptr<B> wptr(sptr);
std::cout << wptr.use_count() << std::endl; // 1
wptr.reset();
std::cout << wptr.use_count() << std::endl; // 0

Учебник C++ (Qt Creator и MinGW)
Учебник C++ (Qt Creator и MinGW) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов

cpp