12.9 – Деструкторы
Деструктор – это еще один особый вид функции-члена класса, которая выполняется при уничтожении объекта этого класса. В то время как конструкторы предназначены для инициализации класса, деструкторы предназначены для помощи в очистке.
Когда объект выходит за пределы области видимости, или динамически выделенный объект явно удаляется с помощью ключевого слова delete
, автоматически вызывается деструктор класса (если он существует) для выполнения очистки, необходимой перед удалением объекта из памяти. Для простых классов (тех, которые просто инициализируют значения обычных переменных-членов) деструктор не нужен потому, что C++ автоматически очистит память за вас.
Однако если ваш объект класса содержит какие-либо ресурсы (например, динамическую память или дескриптор файла или базы данных), или если вам нужно выполнить какое-либо действие до того, как объект будет уничтожен, деструктор – идеальное место для этого, поскольку обычно это последнее, что выполняется перед уничтожением объекта.
Именование деструктора
Как и у конструкторов, у деструкторов есть особые правила именования:
- деструктор должен иметь то же имя, что и класс, но с тильдой (
~
) в начале; - деструктор не может принимать аргументы;
- деструктор не имеет возвращаемого типа.
Обратите внимание, что правило 2 подразумевает, что для каждого класса может существовать только один деструктор, поскольку нет возможности перегружать деструкторы, так как они не могут отличаться друг от друга на основе аргументов.
Как правило, вы не должны вызывать деструктор явно (так как при уничтожении объекта он будет вызываться автоматически), поскольку вы редко когда захотите очистить объект более одного раза. Однако деструкторы могут безопасно вызывать другие функции-члены, поскольку объект не уничтожится до тех пор, пока не выполнится деструктор.
Пример деструктора
Давайте посмотрим на простой класс, который использует деструктор:
#include <iostream>
#include <cassert>
#include <cstddef>
class IntArray
{
private:
int *m_array{};
int m_length{};
public:
IntArray(int length) // конструктор
{
assert(length > 0);
m_array = new int[static_cast<std::size_t>(length)]{};
m_length = length;
}
~IntArray() // деструктор
{
// удаляем динамически выделенный ранее массив
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()
{
IntArray ar(10); // выделяем 10 чисел int
for (int count{ 0 }; count < ar.getLength(); ++count)
ar.setValue(count, count+1);
std::cout << "The value of element 5 is: " << ar.getValue(5) << '\n';
return 0;
} // ar здесь уничтожается, поэтому здесь вызывается функция деструктора ~IntArray()
Совет
Если вы скомпилируете приведенный выше пример и получите следующую ошибку:
error: 'class IntArray' has pointer data members [-Werror=effc++]|
error: but does not override 'IntArray(const IntArray&)' [-Werror=effc++]|
error: or 'operator=(const IntArray&)' [-Werror=effc++]|
то можете либо удалить флаг "-Weffc++
" из настроек компиляции для этого примера, либо добавить в класс следующие две строки:
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;
Мы обсудим, что они делают в «13.14 – Конструкторы преобразования, explicit
и delete
».
Эта программа дает следующий результат:
The value of element 5 is: 6
В первой строке мы создаем экземпляр нового объекта класса IntArray
с именем ar
и передаем длину 10. Это вызывает конструктор, который динамически выделяет память для массива-члена. Здесь мы должны использовать динамическое выделение памяти, потому что во время компиляции не знаем, какова будет длина массива (это решает вызывающий).
В конце main()
объект ar
выходит за пределы области видимости. Это вызывает вызов деструктора ~IntArray()
, который удаляет массив, который мы выделили в конструкторе!
Время выполнения конструктора и деструктора
Как упоминалось ранее, конструктор вызывается при создании объекта, а деструктор – при уничтожении объекта. В следующем примере мы используем инструкции cout
внутри конструктора и деструктора, чтобы показать это:
class Simple
{
private:
int m_nID{};
public:
Simple(int nID)
: m_nID{ nID }
{
std::cout << "Constructing Simple " << nID << '\n';
}
~Simple()
{
std::cout << "Destructing Simple" << m_nID << '\n';
}
int getID() { return m_nID; }
};
int main()
{
// Размещаем Simple в стеке
Simple simple{ 1 };
std::cout << simple.getID() << '\n';
// Размещаем Simple динамически
Simple *pSimple{ new Simple{ 2 } };
std::cout << pSimple->getID() << '\n';
// Мы разместили pSimple динамически, поэтому должны удалить его.
delete pSimple;
return 0;
} // здесь simple выходит из области видимости
Эта программа дает следующий результат:
Constructing Simple 1
1
Constructing Simple 2
2
Destructing Simple 2
Destructing Simple 1
Обратите внимание, что «Simple 1» уничтожается после «Simple 2», потому что мы удалили pSimple
еще до конца функции, тогда как simple
не был уничтожен до конца main()
.
Глобальные переменные создаются перед main()
и уничтожаются после main()
.
RAII
RAII (Resource Acquisition Is Initialization, получение ресурса есть инициализация) – это метод программирования, при котором использование ресурсов привязано к времени жизни объектов с автоматической продолжительностью (например, нединамически выделяемые объекты). В C++ RAII реализован через классы с конструкторами и деструкторами. Ресурс (например, память, дескриптор файла или базы данных и т.д.) обычно приобретается в конструкторе объекта (хотя он может быть получен после создания объекта, если это имеет смысл). Затем этот ресурс можно использовать, пока объект жив. Ресурс освобождается в деструкторе при уничтожении объекта. Основное преимущество RAII заключается в том, что он помогает предотвратить утечку ресурсов (например, не освобождение памяти), поскольку все объекты, содержащие ресурсы, очищаются автоматически.
Класс IntArray
в примере выше является примером класса, который реализует RAII – выделение в конструкторе, освобождение в деструкторе. std::string
и std::vector
– классы стандартной библиотеки, которые следуют принципу RAII – динамическая память приобретается при инициализации и автоматически очищается при уничтожении.
Предупреждение о функции exit()
Обратите внимание, что если вы используете функцию exit()
, ваша программа завершится, но деструкторы не будут вызываться. Будьте осторожны, если вы полагаетесь на деструкторы для выполнения необходимой по очистке работы (например, записи чего-либо в файл журнала или базу данных перед выходом).
Резюме
Как видите, когда конструкторы и деструкторы используются вместе, ваши классы могут инициализироваться и очищаться после себя, без необходимости выполнения программистом какой-либо специальной работы! Это снижает вероятность ошибки и упрощает использование классов.