На цьому уроці ми розглянемо, що таке деструктори в мові С++, навіщо вони потрібні, як їх використовувати і нюанси, які можуть виникнути при їх використанні.
Деструктори
Деструктор — це спеціальний тип методу класу, який виконується при видаленні об’єкта класу. У той час як конструктори призначені для ініціалізації класу, деструктори призначені для очищення пам’яті після нього.
Коли об’єкт автоматично виходить з області видимості або динамічно виділений об’єкт явно видаляється за допомогою ключового слова delete, викликається деструктор класу (якщо він існує) для виконання необхідного очищення до того, як об’єкт буде видалений з пам’яті. Для простих класів (тих, які тільки ініціалізують значення звичайних змінних-членів) деструктор не потрібен, так як C++ автоматично виконає очищення самостійно.
Проте, якщо об’єкт класу містить будь-які ресурси (наприклад, динамічно виділену пам’ять або файл/базу даних), або, якщо вам необхідно виконати будь-які дії до того, як об’єкт буде знищений, деструктор є ідеальним рішенням, оскільки він виконує останні дії з об’єктом перед його остаточним знищенням.
Імена деструкторів
Так само, як і конструктори, деструктори мають свої правила, які стосуються їхніх імен:
деструктор повинен мати те ж ім’я, що і клас, зі знаком тильда (~
) на самому початку;
деструктор не може приймати аргументи;
деструктор не має типу повернення.
З другого правила випливає ще одне правило: для кожного класу може існувати тільки один деструктор, так як немає можливості перевантажити деструктори, як функції, і відрізнятися один від одного аргументами вони не можуть.
Приклад використання деструктора на практиці
Розглянемо простий клас з деструктором:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> #include <cassert> class Massiv { private: int *m_array; int m_length; public: Massiv(int length) // конструктор { assert(length > 0); m_array = new int[length]; m_length = length; } ~Massiv() // деструктор { // Динамічно видаляємо масив, який виділили раніше delete[] m_array ; } void setValue(int index, int value) { m_array[index] = value; } int getValue(int index) { return m_array[index]; } int getLength() { return m_length; } }; int main() { Massiv arr(15); // виділяємо 15 цілочисельних значень for (int count=0; count < 15; ++count) arr.setValue(count, count+1); std::cout << "The value of element 7 is " << arr.getValue(7); return 0; } // об'єкт arr видаляється тут, тому деструктор ~Massiv() викликається також тут |
Результат виконання програми:
The value of element 7 is 8
У першому рядку функції main() ми створюємо новий об’єкт класу Massiv з ім’ям arr
і передаємо довжину (length
) 15. Це призводить до виклику конструктора, який динамічно виділяє пам’ять для масиву класу (m_array
). Ми повинні тут використовувати динамічне виділення, оскільки на момент компіляції ми не знаємо довжини масиву (це значення нам передає caller).
В кінці функції main() об’єкт arr
виходить з області видимості. Це призводить до виклику деструктора ~Massiv()
і до видалення масиву, який ми виділили раніше в конструкторі!
Виконання конструкторів і деструкторів
Як ми вже знаємо, конструктор викликається при створенні об’єкта, а деструктор — при його знищенні. У наступному прикладі ми будемо використовувати стейтменти з cout всередині конструктора і деструктора для відображення їх часу виконання:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> class Another { private: int m_nID; public: Another(int nID) { std::cout << "Constructing Another " << nID << '\n'; m_nID = nID; } ~Another() { std::cout << "Destructing Another " << m_nID << '\n'; } int getID() { return m_nID; } }; int main() { // Виділяємо об'єкт класу Another зі стеку Another object(1); std::cout << object.getID() << '\n'; // Виділяємо об'єкт класу Another динамічно з купи Another *pObject = new Another(2); std::cout << pObject->getID() << '\n'; delete pObject; return 0; } // об'єкт object виходить з області видимості тут |
Результат виконання програми:
Constructing Another 1
1
Constructing Another 2
2
Destructing Another 2
Destructing Another 1
Зверніть увагу, Another 1
знищується після Another 2
, так як ми видалили pObject
до завершення виконання функції main(), тоді як об’єкт object
не був видалений до кінця main().
Ідіома програмування RAII
Ідіома RAII (англ. «Resource Acquisition Is Initialization» = «Отримання ресурсу є ініціалізація») — це ідіома об’єктно-орієнтованого програмування, при якій використання ресурсів прив’язується до часу життя об’єктів з автоматичною тривалістю життя. У мові C++ ідіома RAII реалізується через класи з конструкторами і деструкторами. Ресурс (наприклад, пам’ять, файл або база даних) зазвичай отримується в конструкторі об’єкта (хоча цей ресурс може бути отриманий і після створення об’єкта, якщо в цьому є сенс). Потім цей ресурс можна використовувати, поки об’єкт живий. Ресурс звільняється в деструкторі при знищенні об’єкта. Основною перевагою RAII є те, що це допомагає запобігти витоку ресурсів (наприклад, пам’яті, яка не була звільнена), так як всі об’єкти, що містять ресурси, автоматично очищаються.
В рамках ідіоми програмування RAII об’єкти, які мають ресурси, не повинні динамічно виділятися, так як деструктори викликаються тільки при знищенні об’єктів. Для об’єктів, виділених зі стеку, це відбувається автоматично, коли об’єкт виходить з області видимості, тому немає необхідності турбуватися про те, що ресурс в кінцевому результаті не буде очищений. Однак за очистку динамічно виділених об’єктів, які виділяються з купи, вже користувач несе відповідальність: якщо він забув її виконати, деструктор викликатися не буде, і пам’ять як для об’єкта класу, так і для керованого ресурсу буде втрачена — станеться витік пам’яті!
Клас Massiv з програми, наведеної на початку цього уроку, є прикладом класу, який реалізує принципи RAII: виділення в конструкторі, звільнення в деструкторі. std::string і std::vector — це приклади класів зі Стандартної бібліотеки С++, які слідують принципам RAII: динамічна пам’ять виділяється при ініціалізації і автоматично звільняється при знищенні.
Правило: Використовуйте ідіому програмування RAII і не виділяйте об’єкти вашого класу динамічно.
Попередження про функцію exit()
Якщо ви використовуєте функцію exit(), то ваша програма завершиться, і ніякі деструктори НЕ будуть викликані. Будьте обережні, якщо в такому випадку ви покладаєтеся на свої деструктори для виконання необхідної роботи по очищенню (наприклад, перед тим, як вийти, ви записуєте що-небудь в лог-файл або в базу даних).
Висновки
Використовуючи конструктори і деструктори, ваші класи можуть виконувати ініціалізацію і очищення після себе автоматично без вашої участі! Це зменшує ймовірність виникнення помилок і спрощує процес використання класів.