Поделиться через


Деструкторы (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

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

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

Граф наследования, показывающий виртуальные базовые классы.

Пять классов, помеченные 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 не может освободить эту память, так как она не выделена из свободного хранилища (дополнительные сведения см. в разделе Операторы 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

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

См. также

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