Робота з покажчиками іноді може привести до помилок, якщо використовувати їх не дуже акуратно. Так, наприклад, можу виникнути складності, якщо забути їх проинициализировать, забути їх видалити після закінчення роботи, а також видалити двічі один і той же покажчик.
Основна проблема при використанні покажчиків полягає в тому, що при роботі з динамічною пам'яттю необхідно розробити таку систему, яка буде керувати своєю власною пам'яттю. Вона повинна буде знати, де виділити пам'ять, а де вивільнити, що не завжди є тривіальної завданням. Часом розробляються додатки бувають настільки складними, що не вдається звільнити займану пам'ять рівно там же, де і виділили. І часто складно визначити час життя використовуваного ресурсу.
Класи в C ++ - «розумні». На відміну від мови C у них крім методів є конструктори і деструктори. Таким чином, з'являється поняття «час життя» - час від виклику конструктора до виклику деструктора. Хоча насправді об'єкт вважається живим після повного виконання конструктора до початку виклику деструктора. Не можна говорити, що об'єкт живий під час їх виконання, оскільки не гарантується те, що всі поля є проініціалізувати, і нетривіальна робота з ними може бути чревата проблемами.
void foo () A a;
// корисна робота
// виклик деструктора
>
void foo () A a;
// корисна робота
// виклик деструктора
>
// корисна робота
>
bar (A ());
// деструктор буде
// викликаний після
// завершення
// виконання bar ()
При динамічному виділенні об'єкт буде існувати до тих пір, поки ми його не видалимо. Коли програма закінчить своє виконання, ніхто не викличе деструктор тих об'єктів, що не програміст не видалить сам, тому якщо там знаходиться який-небудь важливий код (наприклад, закриття з'єднання), то він не буде виконаний. В цьому випадку час життя об'єкта обмежується видаленням покажчика за допомогою delete. Якщо об'єкт не видалити до виходу з його зони видимості, то він перетворюється в «зомбі» - він існує, але доступу до нього отримати не можна.
void foo () A a = new A ();
// об'єкт не буде знищений, поки не буде викликаний delete
>
«Розумний покажчик» пов'язує час життя об'єкта в динамічної пам'яті з часом життя об'єкта, виділеному на стеку.
Розглянемо в порівнянні код, який працює зі звичайним покажчиком і розробляються розумним. Будемо грунтуватися вже на розробленому раніше класі BigInt.
BigInt * p = new BigInt ();
(* P) = n;
//.
string str = p-> toString ();
//.
delete p;
Проблема використання звичайного покажчика полягає в тому, що де-небудь в тексті може бути викликаний return. і в цьому випадку програма не дійде до місця видалення. Проте, delete повинен викликатися завжди, коли відбулося виділення пам'яті за допомогою new. Можна позбутися від необхідності викликати його вручну, скориставшись розумним покажчиком, синтаксис спілкування з яким схожий з синтаксисом спілкування зі звичайним покажчиком. В цьому випадку очищення пам'яті можна перенести в деструктор розумного покажчика.
Опишемо клас розробляється розумного покажчика.
private:
BigInt * ptr_;
public:
BigIntPtr (BigInt * p = 0);
BigIntPtr ();
BigInt operator * () const;
BigInt * operator -> () const;
private:
BigIntPtr (BigIntPtr const );
BigIntPtr operator = (BigIntPtr const );
>;
BigIntPtr :: BigIntPtr (BigInt * p): ptr_ (p)>
BigIntPtr () delete ptr_;
>
BigInt BigIntPtr :: operator * () const return * ptr_;
>
BigInt * BigIntPtr :: operator -> () const return ptr_;
>
Слід також зазначити, що відповідно до мнемонічному правилом при визначенні деструкції необхідно також визначити конструктор копіювання і оператор присвоювання. Це пояснюється тим, що якщо нам потрібно нетривіально знищити об'єкт (наприклад, звільнити раніше зайняту пам'ять), то і при копіюванні за допомогою конструктора або при присвоєнні можуть виникнути проблеми.
Унарний оператор «зірочка» - константних, але повертає неконстантную посилання. Оскільки розроблюваний розумний покажчик повинен вести себе як звичайний покажчик, то ми повинні дозволити змінювати той об'єкт, на який посилаємося. Однак ми маємо на увазі, що сам розумний покажчик в цьому випадку не змінюється.
Немає потреби обробляти виняткову ситуацію, якщо розумний покажчик ні на що не посилається (тобто ptr_ дорівнює нулю). Справа в тому, що звичайний покажчик також ніяк не реагує на таку поведінку, та й не зрозуміло, що слід повертати в цьому випадку розумному.
Оператор «стрілочка» повинна повертати покажчик. Хоча насправді, вона може повернути що завгодно, у чого є оператор «стрілочка», але тоді виклик цього оператора буде циклічно повторюватись до тих пір, поки, нарешті, не буде повернений покажчик.
Деструкція розумного покажчика позбавляє нас від необхідності очищати пам'ять, оскільки займається цим сам. Таким чином, об'єкт, на який посилається розумний покажчик, буде знищений відразу при виході з блоку видимості розумного покажчика.
Дана концепція носить назву RAII - Resource Acquisition Is Initialization (Виділення ресурсу є ініціалізація). Це означає те, що ініціалізацію ми покладаємо на сам об'єкт при виділенні пам'яті на нього, а також робимо його відповідальним за повернення пам'яті при його знищенні. Як приклад можна привести з'єднання з базами даних, запитані порти, м'ютекси в паралельних програмах. Якщо об'єкт не поверне запитаний при ініціалізації ресурс, то ніхто після нього не зможе ним скористатися.
Розроблений вище код має безліч недоліків. По-перше, такий покажчик можна змінити. Це можна виправити, визначивши оператор присвоювання, правою частиною у якого є покажчиком на потрібний об'єкт.
Таким чином, не може існувати два покажчика на один об'єкт. Деструкція для першого розумного покажчика знищить об'єкт, і деструкція другого покажчика спробує зробити те ж саме. В цьому випадку відбудеться помилка подвійного видалення.
Крім цього, неможливо перевірити, чи не посилається такий розумний покажчик на нуль.
Також такі покажчики не можна порівнювати зі звичайними покажчиками. Якщо передати їх у функцію порівняння, то в разі, якщо конструктор, що приймає звичайний покажчик, оголошений не як explicit. то створиться розумний покажчик, а після виходу з неї деструктор тимчасового об'єкта видалить сам об'єкт.
Концепцію такого покажчика прийнято називати Scoped Pointer - покажчик області видимості. Такий розумний покажчик можливо використовувати лише в блоці, якому він оголошений.
Інша реалізація - Shared Pointer, що розділяється покажчик. Його відмінність полягає в тому, що такий покажчик можна скопіювати. При цьому сам об'єкт віддалиться лише тоді, коли буде знищений останній покажчик, який посилається на нього. Це стає можливим при використанні лічильника посилань.
Ідея полягає в наступному: коли створюється розумний покажчик, то він в свою чергу створює лічильник посилань, деяке число, яке зберігається в динамічної пам'яті, що визначає кількість розумних покажчиків, що посилаються на об'єкт. При копіюванні покажчика відбувається не тільки копіювання покажчика на об'єкт, але і покажчика на цей самий лічильник, а його значення збільшується. При знищенні розумного покажчика значення лічильника зменшується. Якщо знищуваний покажчик був останнім власником даного об'єкту, то і об'єкт віддаляється теж.
Інша варіація розділяється покажчика це створення додаткового об'єкта в динамічної пам'яті, який містить в собі і лічильник, і посилання на зберігається об'єкт. Таким чином, при копіюванні буде копіюватися лише один покажчик, кожен розумний покажчик буде посилатися тільки на один об'єкт з лічильником і посиланням на оригінал.
Такі покажчики вже можна привласнювати один одному, передавати копією в функції, порівнювати і багато іншого.