C++. Умные указатели. Классы указателей unique_ptr, shared_ptr, weak_ptr

Умные указатели. Классы указателей unique_ptr, shared_ptr, weak_ptr

Перед изучением данной темы рекомендуется ознакомиться со следующей темой:


Содержание


Поиск на других ресурсах:

1. Использование умных указателей в C++

В современном C++ стандартная библиотека (Standard Library) включает в себя так называемые «умные указатели» (smart pointers). Эти указатели предназначены для того, чтобы безопасно (без исключительных ситуаций) обеспечить освобождение памяти и избежать утечки ресурсов.

Умные указатели определены в пространстве имен std и в заголовочном файле <memory>.

Основная цель использования умных указателей – обеспечить получение ресурса одновременно с инициализацией объекта в одной строке кода (а не в нескольких, как это было раньше с использованием так называемых обычных необработанных (raw) указателей).

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

Если нужно инициализировать необработанный (raw) указатель по адресу реального ресурса или дескриптору ресурса, то рекомендуется передавать этот необработанный указатель умному указателю. Чтобы избежать невидимых ошибок при выделении/освобождении памяти, рекомендуется использовать необработанные указатели (raw pointers) в небольших блоках кода с ограниченной областью действия, циклах или вспомогательных функциях. В небольших блоках кода использование необработанных указателей дает максимальную производительность и уменьшает неразбериху во владении памятью.

Подобная идиома реализована в языках C#, Java.

 

2. Виды умных указателей

Ниже представлены виды умных указателей, которые нужно использовать для того, чтобы инкапсулировать простые старые (необработанные) указатели C++.

2.1. Указатель unique_ptr

Указатель типа unique_ptr объявляется в пространстве имен std. Обращение к указателю может быть std::unique_ptr.
Этот указатель может инкапсулировать (обладать) только один указатель старого типа. Как правило, этот указатель используется, если нет необходимости использовать указатели типа shared_ptr.
Указатель unique_ptr может быть передан (перемещен) новому владельцу, но не скопирован или разделен для общего доступа. Он заменяет уже устаревший auto_ptr. Этот указатель небольшой по размеру (один указатель) и поддерживает rvalue-ссылку.
Данный указатель используется для быстрой вставки и извлечения данных из коллекций C++.

В наиболее общем случае объявление указателя unique_ptr выглядит следующим образом:

unique_ptr<T> ptr(rawPtr);

здесь

  • ptr – указатель типа unique_ptr;
  • rawPtr – необработанный (raw) указатель на тип T;
  • T – тип значения, на которое указывают указатели ptr и rawPtr.

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

unique_ptr<int> p1(new int(25));
unique_ptr<int> p2;
p2 = p1; // запрещено, ошибка

вызовет ошибку на этапе компиляции. То же самое касается случая объявления указателя и его инициализации.

Указатель типа unique_ptr имеет следующие основные методы:

  • get() – возвращает стандартный указатель на объект на основе указателя unique_ptr без передачи прав владения на этот объект. После вызова метода указатель unique_ptr продолжает указывать на тот же объект;
  • release() – возвращает стандартный указатель на основе указателя unique_ptr с передачей прав владения. После вызова метода значение указателя unique_ptr равно nullptr;
  • reset() – сбрасывает право владения на объект. После вызова метода значение указателя unique_ptr равно nullptr.

 

2.2. Указатель shared_ptr

Указатель shared_ptr реализован в пространстве имен std. Полное обращение к указателю std::shared_ptr.
Это умный указатель с подсчетом количества ссылок. Данный указатель используется, когда нужно назначить один необработанный указатель нескольким владельцам. Например, при возврате копии объекта из контейнера при условии сохранения оригинала.
Указатель shared_ptr реализует подсчет количества ссылок на ресурс. Ресурс освобождается только тогда, когда количество ссылок на него будет равно 0. По такому правилу работает сборщик мусора.

В наиболее общем случае создание указателя типа shared_ptr может производиться одним из двух способов:

shared_ptr<T> ptr;
shared_ptr<T> ptr(rawPtr);

здесь

  • ptr – имя указателя типа shared_ptr, который нужно создать;
  • T – тип данных, на который указывает указатель;
  • rawPtr – стандартный указатель на данные типа T (объект типа T). Этот указатель служит в качестве исходного для получения указателя ptr типа shared_ptr<T>.

Для указателя shared_ptr определены следующие методы:

  • get() – возвращает стандартный указатель на основе указателя shared_ptr без передачи прав на объект. После вызова метода указатель shared_ptr указывает на тот же объект, что и до вызова;
  • swap() – обменивает местами значения двух указателей;
  • reset() – сбрасывает права владения на объект;
  • use_count() – возвращает количество копий указателей на объект. Учитываются также стандартные указатели, полученные методом get();
  • операторная функция operator=() реализующая присваивание указателей типа shared_ptr.

 

2.3. Указатель weak_ptr

Указатель weak_ptr реализован в пространстве имен std. Соответственно, обращение к указателю имеет вид std::weak_ptr.
Указатель std::weak_ptr – это умный указатель, содержащий так называемую «слабую» ссылку на объект на который указывает указатель типа shared_ptr. Термин «слабый» означает, что указатель weak_ptr не является владельцем объекта, на который он указывает. Владелец этого объекта — указатель shared_ptr.
Указатель weak_ptr определяет временное владение – это случай, когда к объекту нужно получить доступ только тогда, когда на него уже указывает указатель shared_ptr, имеющий право владения на этот объект. При этом изменять значение объекта с помощью weak_ptr не получится. Значение объекта можно только считывать.

Чтобы с помощью указателя weak_ptr можно было изменить значение объекта на который он указывает, нужно его конвертировать в указатель shared_ptr с помощью метода lock(). В этом случае, если исходный указатель shared_ptr уничтожается, то время жизни объекта продолжается до тех пор, пока не будет уничтожен временный shared_ptr, который был получен из weak_ptr.
Указатель weak_ptr полезен в случае, когда нужно обрабатывать несколько ссылок, циклически указывающих на объекты управляемые указателями shared_ptr. Если в таком цикле ссылок нет общих внешних указателей, то счетчик shared_ptr не может достичь нуля. В результате происходит утечка памяти (memory leak). Чтобы избежать этой ситуации, один из указателей в цикле устанавливается как weak_ptr.

В общем случае получение указателя типа weak_ptr из указателя типа shared_ptr выглядит следующим образом

shared_ptr<T> pShared(pRaw);
weak_ptr<T> pWeak = pShared;

здесь

  • pRaw – необработанный (raw) указатель, указывающий на данные типа T;
  • pShared – указатель типа shared_ptr;
  • pWeak – указатель типа weak_ptr;
  • T – тип данных, на которые указывают указатели pRaw, pShared, pWeak.

Для указателя типа weak_ptr определены следующие методы:

  • lock() – конвертирует указатель типа weak_ptr в указатель типа shared_ptr;
  • swap() – обменивает местами указатели типа weak_ptr;
  • операторная функция operator=() осуществляющая присваивание указателей типа weak_ptr.

 

3. Пример, демонстрирующий использование указателя unique_ptr

В примере показано:

  • создание указателя типа unique_ptr;
  • конвертирование (копирование) указателя unique_ptr в стандартный указатель с помощью функции get();
  • сброс значения по указателю unique_ptr с помощью функции reset();
  • передача прав стандартному (raw) указателю с помощью функции release().

 

#include <iostream>
using namespace std;

void main()
{
  // Указатель unique_ptr – замена auto_ptr, работает как классический указатель
  // 1. Объявить указатель типа unique_ptr, указывающий на число 55
  unique_ptr<int> pI(new int(55));

  // 2. Получить классический указатель с помощью функции get()
  int* p;
  p = pI.get(); // указатели p и pI указывают на одну и ту же область памяти
  cout << "*p = " << *p << endl; // *p = 55
  cout << "*pI = " << *pI << endl; // *pI = 55

  // 3. Изменить значение по указателю *p
  *pI = 88;
  cout << "*p = " << *p << endl; // *p = 88
  cout << "*pI = " << *pI << endl; // *pI = 88

  // 4. Сбросить права владения функцией reset(),
  // здесь освобождается память, выделенная для указателя
  pI.reset(); // освобождается значение 88
  cout << "*p = " << *p << endl; // *p = -572662307 - указатель не указывает на 88, pI => nullptr
  if (pI == nullptr)
    cout << "pI == nullptr" << endl; // +
  else
    cout << "pI != nullptr" << endl;

  // 5. Демонстрация функции release()
  // 5.1. Объявить указатель unique_ptr
  unique_ptr<int> pI2(new int(333)); // pI2 => 333

  // 5.2. Передать права указателю p2 с сбросом прав из указателя pI2
  int* p2 = pI2.release(); // p2 => 333, pI2 => nullptr - освобождается

  // 5.3. Вывести значение указателей
  cout << "*p2 = " << *p2 << endl; // *p2 = 333

  if (pI2 == nullptr)
    cout << "pI2 == nullptr" << endl; // +
  else
    cout << "pI2 != nullptr " << endl;

  // 6. Попытка передачи прав другому указателю unique_ptr
  //unique_ptr<int> pI3 = pI2; // ошибка компиляции, запрещено
}

Результат выполнения программы

*p = 55
*pI = 55
*p = 88
*pI = 88
*p = -572662307
pI == nullptr
*p2 = 333
pI2 == nullptr

 

4. Пример, демонстрирующий использование указателя shared_ptr

В примере демонстрируется использование указателя shared_ptr.

#include <iostream>
using namespace std;

void main()
{
  // 1. Объявить 2 указателя типа shared_ptr и заполнить их данными
  shared_ptr<float> pF1(new float(2.8f));
  shared_ptr<float> pF2(new float(7.55f));

  // 2. Объявить указатель, указывающий на pF1
  shared_ptr<float> pF3 = pF1; // pF1 не освободился, pF также указывает 2.8f

  // Теперь указатели pF3 и pF1 указывают на одинаковую область памяти,
  // в которой записано число 2.8f

  // 3. Вывести все указатели
  cout << "pF1 => " << *pF1 << endl; // pF1 => 2.8
  cout << "pF2 => " << *pF2 << endl; // pF2 => 7.55
  cout << "pF3 => " << *pF3 << endl; // pF3 => 2.8

  // 4. Здесь освобождается объект 7.55
  pF2 = pF3;

  // 5. Изменить значение pF2 и вывести значение всех указателей
  *pF2 = 8.99f;
  cout << "pF1 => " << *pF1 << endl; // pF1 => 8.99
  cout << "pF2 => " << *pF2 << endl; // pF2 => 8.99
  cout << "pF3 => " << *pF3 << endl; // pF3 => 8.99

  // 6. Указатель типа shared_ptr. Метод swap()
  shared_ptr<int> p1(new int(33));
  shared_ptr<int> p2(new int(77));

  // Обменять местами p1 <=> p2
  p1.swap(p2); // p1 => 77, p2 => 33
  cout << *p1 << endl; // 77
  cout << *p2 << endl; // 33

  // 7. Метод get(). Конвертировать в стандартный int*
  int* pI = p1.get();
  cout << *pI << endl; // 77
  cout << *p1 << endl;

  // 8. Метод reset() - сбросить права владения на объект
  p2.reset();
  if (p2 == nullptr)
    cout << "p2 == nullptr" << endl; // +
  else
    cout << "p2 != nullptr" << endl;

  // 9. Метод use_count() - возвращает количество копий указателей на объект
  cout << p1.use_count() << endl; // 1
} // здесь освобождаются все указатели

Результат выполнения программы

pF1 => 2.8
pF2 => 7.55
pF3 => 2.8
pF1 => 8.99
pF2 => 8.99
pF3 => 8.99
77
33
77
77
33
p2 == nullptr

 

5. Пример, демонстрирующий использование указателя weak_ptr

В примере показано:

  • создание указателя weak_ptr на основе указателя типа shared_ptr;
  • конвертирование указателя weak_ptr в указатель shared_ptr с помощью метода lock();
  • использование метода use_count() для получения количества копий указателей, указывающих на данные;
  • невозможность доступа по значению для указателя weak_ptr.

 

#include <iostream>
using namespace std;

void main()
{
  // Указатель weak_ptr
  // 1. Объявить указатель типа shared_ptr
  shared_ptr<int> pShared1(new int(25)); // pI1 => 25

  // 2. Объявить указатель типа weak_ptr который указывает на число 25
  weak_ptr<int> pWeak = pShared1;
  cout << "*pI2.use_count() = " << pWeak.use_count() << endl; // pI2.use_count() = 1

  // 3. Конвертировать указатель weak_ptr в shared_ptr
  shared_ptr<int> pShared2 = pWeak.lock();
  cout << "*pI2.use_count() = " << pWeak.use_count() << endl; // pI2.use_count() = 2

  // 4. Вывести значение по указателю pShared2
  cout << "*pShared2 = " << *pShared2 << endl;

  // 5. Вывести значение по указателю pWeak не получится
  // cout << *pWeak << endl; - ошибка компиляции

  // 6. Метод swap() - обменять указатели типа weak_ptr
  weak_ptr<int> pw1;
  weak_ptr<int> pw2;
  shared_ptr<int> ps1(new int(38));
  shared_ptr<int> ps2(new int(55));
  pw1 = ps1; // pw1 => 38
  pw2 = ps2; // pw2 => 55

  pw1.swap(pw2);

  cout << "pw1 => " << *pw1.lock().get() << endl; // pw1 => 55
  cout << "pw2 => " << *pw2.lock().get() << endl; // pw2 => 38
}

Результат выполнения программы

*pI2.use_count() = 1
*pI2.use_count() = 2
*pShared2 = 25
pw1 => 55
pw2 => 38

 


Связанные темы