Перейти к содержимому

Виртуальный деструктор c зачем нужен

  • автор:

Почему деструктор базового класса должен объявляться виртуальным?

Давайте разберемся, зачем нужны виртуальные методы. Рассмотрим следующий код:

class Foo < public: void f(); >; class Bar : public Foo < public: void f(); >Foo *p = new Bar(); p->f(); 

Вызывая p->f() , мы обращаемся к Foo::f() . Это потому, что р — указатель на Foo, a f() — невиртуальная функция.

Чтобы гарантировать, что p->f() вызовет нужную реализацию f(), необходимо объявить f() как виртуальную функцию.

Теперь вернемся к деструктору. Деструкторы предназначены для очистки памяти и ресурсов. Если деструктор Foo не является виртуальным, то при уничтожении объект Bar все равно будет вызван деструктор базового класса Foo.

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

Разбор взят из книги Гейл Л. Макдауэлл «Cracking the Coding Interview» (есть в переводе).

Зачем нужен виртуальный деструктор? [дубликат]

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

#include using namespace std; struct Base < Base() < cout ~Base() < cout >; struct Derived: public Base < Derived() < cout ~Derived() < cout >; int main()

Что вы могли ожидать на выходе:

Base() Derived() ~Derived() ~Base() 

Что может произойти (может, потому что, в общем случае, это undefined behaviour):

Base() Derived() ~Base() 

Для устранения данной проблемы необходимо деструктор класса родителя объявить виртуальным ( virtual ~Base() ), что позволит компилятору добраться до деструктора наследника по таблице виртуальных функций.

Виртуальный деструктор в C++

В языке программирования C++ деструктор полиморфного базового класса должен объявляться виртуальным. Только так обеспечивается корректное разрушение объекта производного класса через указатель на соответствующий базовый класс.

Рассмотрим следующий пример.

#include using namespace std; // Вспомогательный класс class Object < public: Object() < cout ~Object() < cout >; // Базовый класс class Base < public: Base() < cout virtual ~Base() < cout virtual void print() = 0; >; // Производный класс class Derived: public Base < public: Derived() < cout ~Derived() < cout void print() <> Object obj; >; int main ()

В функции main указателю на базовый класс присваивается адрес динамически создаваемого объекта производного класса Derived. Затем через этот указатель объект разрушается. При этом наличие виртуального деструктора базового класса обеспечивает вызовы деструкторов всех классов в ожидаемом порядке, а именно, в порядке, обратном вызовам конструкторов соответствующих классов.

Вывод программы с использованием виртуального деструктора в базовом классе будет следующим:

Base::ctor() Object::ctor() Derived::ctor() Derived::dtor() Object::dtor() Base::dtor()

Уничтожение объекта производного класса через указатель на базовый класс с невиртуальным деструктором дает неопределенный результат. На практике это выражается в том, что будет разрушена только часть объекта, соответствующая базовому классу. Если в коде выше убрать ключевое слово virtual перед деструктором базового класса, то вывод программы будет уже иным. Обратите внимание, что член данных obj класса Derived также не разрушается.

Base::ctor() Object::ctor() Derived::ctor() Base::dtor()

Когда же следует объявлять деструктор виртуальным? Cуществует правило — если базовый класс предназначен для полиморфного использования, то его деструктор должен объявляться виртуальным. Для реализации механизма виртуальных функций каждый объект класса хранит указатель на таблицу виртуальных функций vptr, что увеличивает его общий размер. Обычно, при объявлении виртуального деструктора такой класс уже имеет виртуальные функции, и увеличения размера соответствующего объекта не происходит.

Если же базовый класс не предназначен для полиморфного использования (не содержит виртуальных функций), то его деструктор не должен объявляться виртуальным.

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

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

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

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

// spec1_destructors.cpp #include // strlen() class String < public: String(const char* ch); // Declare the constructor ~String(); // Declare the destructor private: char* _text; >; // 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::~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 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()
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 не может освободить эту память, так как он не выделяется из свободного хранилища (дополнительные сведения см. в разделе «Новые и удаленные операторы»). Вызов деструктора, однако, может выполнить соответствующую очистку. Для явного вызова деструктора для объекта ( 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 

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *