Деструкторы (C++)

Деструктор — это функция-член, которая вызывается автоматически, когда объект выходит из область или явно уничтожается вызовом delete илиdelete[]. Деструктор имеет то же имя, что и класс и предшествует тильде (~). Например, деструктор для класса String объявляется следующим образом: ~String().

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

Рассмотрим следующее объявление класса String:

// spec1_destructors.cpp
#include <string> // strlen()

class String
{
    public:
        String(const char* ch);  // Declare the constructor
        ~String();               // Declare the destructor
    private:
        char* _text{nullptr};
};

// Define the constructor
String::String(const char* ch)
{
    size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL

    // Dynamically allocate the correct amount of memory.
    _text = new char[sizeOfText];

    // If the allocation succeeds, copy the initialization string.
    if (_text)
    {
        strcpy_s(_text, sizeOfText, ch);
    }
}

// Define the destructor.
String::~String()
{
    // Deallocate the memory that was previously reserved for the string.
    delete[] _text;
}

int main()
{
    String str("We love C++");
}

В предыдущем примере деструктор String::~String использует delete[] оператор для освобождения места динамически выделенного для текстового хранилища.

Объявление деструкторов

Деструкторы — это функции с тем же именем, что и класс, но с добавленным в начало знаком тильды (~).

При объявлении деструкторов действуют несколько правил. Деструкторы:

  • Не принимать аргументы.
  • Не возвращайте значение (или void).
  • Невозможно объявить как const, volatileили static. Однако их можно вызвать для уничтожения объектов, объявленных как const, volatileили static.
  • Можно объявить как virtual. С помощью виртуальных деструкторов можно уничтожить объекты, не зная их тип, — правильный деструктор для объекта вызывается с помощью механизма виртуальной функции. Деструкторы также можно объявить как чистые виртуальные функции для абстрактных классов.

Использование деструкторов

Деструкторы вызываются, когда происходит одно из следующих событий:

  • Локальный (автоматический) объект с областью видимости блока выходит за пределы области видимости.
  • Используется delete для освобождения объекта, выделенного с помощью new. Использование delete[] результатов неопределенного поведения.
  • Используется delete[] для освобождения объекта, выделенного с помощью new[]. Использование delete результатов неопределенного поведения.
  • Время существования временного объекта заканчивается.
  • Программа заканчивается, глобальные или статические объекты продолжают существовать.
  • Деструктор явно вызываться с использованием полного имени функции деструктора.

Деструкторы могут свободно вызывать функции-члена класса и осуществлять доступ к данным членов класса.

Существует два ограничения на использование деструкторов:

  • Вы не можете взять свой адрес.

  • Производные классы не наследуют деструктор базового класса.

Порядок уничтожения

Когда объект выходит за пределы области или удаляется, последовательность событий при его полном уничтожении выглядит следующим образом:

  1. Вызывается деструктор класса, и выполняется тело функции деструктора.

  2. Деструкторы для объектов нестатических членов вызываются в порядке, обратном порядку их появления в объявлении класса. Необязательный список инициализации элементов, используемый в строительстве этих элементов, не влияет на порядок строительства или уничтожения.

  3. Деструкторы для не виртуальных базовых классов вызываются в обратном порядке объявления.

  4. Деструкторы для виртуальных базовых классов вызываются в порядке, обратном порядку их объявления.

// order_of_destruction.cpp
#include <cstdio>

struct A1      { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };

struct B1      { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };

int main() {
   A1 * a = new A3;
   delete a;
   printf("\n");

   B1 * b = new B3;
   delete b;
   printf("\n");

   B3 * b2 = new B3;
   delete b2;
}
A3 dtor
A2 dtor
A1 dtor

B1 dtor

B3 dtor
B2 dtor
B1 dtor

Виртуальные базовые классы

Деструкторы для виртуальных базовых классов вызываются в порядке, обратном их указанию в направленном ациклическом графе (в глубину, слева направо, обход в обратном порядке). На следующем рисунке представлен граф наследования.

Inheritance graph that shows virtual base classes.

Пять классов, помеченные A до E, упорядочены в графе наследования. Класс E является базовым классом B, C и D. КлассЫ C и D являются базовым классом A и B.

Ниже перечислены определения классов для классов, показанных на рисунке:

class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};

Чтобы определить порядок удаления виртуальных базовых классов объекта типа E, компилятор выполняет сборку списка, применяя следующий алгоритм.

  1. Просмотрите левую часть графа, начиная с самой глубокой точки графа (в данном случае E).
  2. Просматривайте граф справа налево, пока не будут пройдены все узлы. Запомните имя текущего узла.
  3. Пересмотрите предыдущий узел (вниз и вправо), чтобы определить, является ли рассматриваемый узел виртуальным базовым классом.
  4. Если рассматриваемый узел является виртуальным базовым классом, просмотрите список, чтобы проверить, был ли он введен ранее. Если он не является виртуальным базовым классом, игнорируйте его.
  5. Если запоминаемый узел еще не находится в списке, добавьте его в нижней части списка.
  6. Просмотрите граф вверх и вдоль следующего пути вправо.
  7. Перейдите к шагу 2.
  8. Если путь последний путь вверх исчерпан, запомните имя текущего узла.
  9. Перейдите к шагу 3.
  10. Выполняйте этот процесс, пока нижний узел снова не станет текущим узлом.

Таким образом, для класса E порядок удаления будет следующим.

  1. Не-виртуальный базовый класс E.
  2. Не-виртуальный базовый класс D.
  3. Не-виртуальный базовый класс C.
  4. Виртуальный базовый класс B.
  5. Виртуальный базовый класс A.

В ходе этого процесса создается упорядоченный список уникальных записей. Имя класса никогда не отображается дважды. После создания списка он выполняется в обратном порядке, а деструктор для каждого класса в списке от последнего до первого вызывается.

Порядок построения или уничтожения в первую очередь важен, если конструкторы или деструкторы в одном классе полагаются на другой компонент, созданный первым или сохраняющимся дольше, например, если деструктор для A (на рисунке, показанном ранее) опирался B на все еще присутствует при выполнении кода или наоборот.

Такие взаимозависимости между классами в графе наследования опасны, поскольку классы, наследуемые впоследствии, могут изменить крайний левый путь, тем самым изменив порядок построения и удаления.

Не виртуальные базовые классы

Деструкторы для не виртуальных базовых классов вызываются в обратном порядке, в котором объявляются имена базовых классов. Рассмотрим следующее объявление класса.

class MultInherit : public Base1, public Base2
...

В предыдущем примере деструктор Base2 вызывается перед деструктором Base1.

Явные вызовы деструктора

Редко возникает необходимость в явном вызове деструктора. Однако может быть полезно выполнить удаление объектов, размещенных по абсолютным адресам. Эти объекты обычно выделяются с помощью определяемого пользователем new оператора, который принимает аргумент размещения. Оператор delete не может освободить эту память, так как он не выделяется из свободного хранилища (дополнительные сведения см. в разделе "Новые и удаленные операторы"). Вызов деструктора, однако, может выполнить соответствующую очистку. Для явного вызова деструктора для объекта (s) класса String воспользуйтесь одним из следующих операторов.

s.String::~String();     // non-virtual call
ps->String::~String();   // non-virtual call

s.~String();       // Virtual call
ps->~String();     // Virtual call

Нотация для явных вызовов деструкторов, показанная в предыдущем примере, может использоваться независимо от того, определяет ли тип деструктор. Это позволяет выполнять такие явные вызовы, не зная, определен ли деструктор для типа. Явный вызов деструктора, если ни один из них не определен, не имеет никакого эффекта.

Отказоустойчивость

Классу требуется деструктор, если он получает ресурс и безопасно управлять ресурсом, который, вероятно, должен реализовать конструктор копирования и назначение копирования.

Если эти специальные функции не определены пользователем, они неявно определяются компилятором. Неявно созданные конструкторы и операторы назначения выполняют неглубокое копирование элементов, что почти наверняка неправильно, если объект управляет ресурсом.

В следующем примере неявно созданный конструктор копирования сделает указатели str1.text и str2.text ссылается на ту же память, и при возврате из copy_strings()нее память будет удалена дважды, что является неопределенным поведением:

void copy_strings()
{
   String str1("I have a sense of impending disaster...");
   String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
  // undefined behavior

Явное определение деструктора, конструктора копирования или оператора назначения копирования предотвращает неявное определение конструктора перемещения и оператора назначения перемещения. В этом случае обычно не удается предоставить операции перемещения, если копирование является дорогостоящим, отсутствует возможность оптимизации.

См. также

Конструкторы копий и операторы присваивания копий
Конструкторы перемещения и операторы присваивания перемещением