C++. Розумні покажчики. Класи покажчиків unique_ptr, shared_ptr, weak_ptr

Розумні покажчики. Класи покажчиків unique_ptr, shared_ptr, weak_ptr

Перед вивченням даної теми рекомендується ознайомитись з наступною темою:


Зміст


Пошук на інших ресурсах:

1. Використання розумних покажчиків у C++

В сучасному C++ стандартна бібліотека (Standard Library) включає так звані “розумні покажчики” (smart pointers). Ці покажчики призначені для того, щоб безпечно (без виключних ситуацій) забезпечити звільнення пам’яті та уникнути витоку ресурсів.

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

Основна мета використання розумних покажчиків забезпечити отримання ресурсу одночасно з ініціалізацією об’єкту в одному рядку коду (а не в декількох, як це було раніше з використанням так званих звичайних необроблених (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.
Це є розумний покажчик з підрахунком кількості посилань. Даний покажчик використовується, коли потрібно назначити один необроблений (raw) покажчик декільком власникам. Наприклад, при поверненні копії об’єкту з контейнера при умові збереження оригіналу.
Покажчик 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();
  • передача прав стандартному покажчику з допомогою функції 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

 


Споріднені теми