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

от admin

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

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

И всё это, чтобы не было никакой утечки памяти.

Так ли я понял суть?

tocic's user avatar

hamsternik's user avatar

Не, немного не так. Деструктор базового класса из деструктора производного вызывать не надо (да и нельзя), он сам вызовется автоматически. Проблема в другом: деструктор производного класса может быть не вызван!

Представьте себе такую ситуацию:

Что случится, если в списке будет один Spy ? А вот что: при уничтожении объекта типа Spy по указателю типа Person* вызовется невиртуальный деструктор

Person . Значит, память под массив gadgets не будет освобождена. Вот вам и утечка памяти.

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

Хуже того, по стандарту отсутствие виртуального деструктора в данном случае является undefined behaviour, то есть, программа имеет право сделать что угодно: отформатировать винчестер, признаться в любви к вашей химичке через «Вконтакте» или подлить валерьянки в миску с Вискасом.

Виртуальные функции и деструктор

Когда-то давным давно я собирался и даже обещал написать про механизм виртуальных функций относительно деструкторов. Теперь у меня наконец появилось свободное время и я решил воплотить эту затею в жизнь. На самом деле эта мини-статья служит «прологом» к моей следующей статье. Но я постарался изложить доходчиво и понятно основные моменты по текущей теме. Если вы чувствуете, что еще недостаточно разобрались в механизме виртуальных вызовов, то, возможно, вам следует для начала прочитать мою предыдущую статью.

Сразу же, как обычно, оговорюсь, что: 1) статья моя не претендует на полноту изложения материала; 2) мегапрограммеры ничего нового здесь не узнают; 3) материал не новый и давно описан во многих книгах, но если явно об этом не прочитать и самому специально не задумываться, то можно о некоторых моментах даже не подозревать (до поры, до времени). Также прошу прощения за надуманные примеры 🙂

Виртуальные деструкторы

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

Основное правило: если у вас в классе присутствует хотя бы одна виртуальная функция, деструктор также следует сделать виртуальным. При этом не следует забывать, что деструктор по умолчанию виртуальным не будует, поэтому следует объявить его явно. Если этого не сделать, у вас в программе почти наверняка будут утечки памяти (memory leaks). Чтобы понять почему, опять же много ума не надо. Рассмотрим несколько примеров.

В первом случае создадим объект производного класса в стеке:

using std :: cout ;
using std :: endl ;

Всем ясно, что вывод программы будет следующим:

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

Попробуем теперь создать тот же объект в динамической памяти, используя при этом указатель на объект базового класса (код классов не изменился, поэтому привожу только код функции main()):

На сей раз конструируется объект так, как и надо, а при разрушении происходит утечка памяти, потому как деструктор производного класса не вызывается:

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

Чтобы этого избежать, деструктор в базовом классе должен быть объявлен как виртуальный:

using std :: cout ;
using std :: endl ;

int main ( )
<
A * pA = new B ;
delete pA ;
return EXIT_SUCCESS ;
>

Теперь-то мы получим желаемый порядок вызовов:

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

Виртуальные функции в деструкторах

Давайте для начала рассмотрим ситуацию с вызовом виртуальных функций внутри класса. Предположим, что у нас есть Кот, который просит покушать мяуканьем, а затем приступает к процессу 🙂 Так поступают многие коты, но не Чеширский! Чеширский, как известно, мало того что вечно улыбается, так еще и довольно разговорчив, поэтому мы научим его говорить, переопределив метод speak():

using std :: cout ;
using std :: endl ;

class Cat
<
public :
void askForFood ( ) const
<
speak ( ) ;
eat ( ) ;
>
virtual void speak ( ) const < cout << "Meow! " ; >
virtual void eat ( ) const < cout << "*champing*" << endl ; >
> ;

class CheshireCat : public Cat
<
public :
virtual void speak ( ) const < cout << "WTF?! Where \' s my milk? =) " ; >
> ;

cout << «Ordinary Cat: » ; cats [ 0 ] — > askForFood ( ) ;
cout << «Cheshire Cat: » ; cats [ 1 ] — > askForFood ( ) ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Вывод этой программы будет следующим:

Рассмотрим код более подробно. Есть класс Cat с парой виртуальных методов, один из которых переопределен в производном CheshireCat. Но всё самое интересное происходит в методе askForFood() класса Cat.

Как видно, метод всего лишь содержит вызовы двух других методов, однако конструкция speak() в данном контексте эквивалента this->speak(), то есть вызов происходит через указатель, а значит — будет использовано позднее связывание. Вот почему при вызове метода askForFood() через указатель на CheshireCat мы видим то, что и хотели: механизм виртуальных функций работает исправно даже несмотря на то, что вызов непосредственно виртуального метода происходит внутри другого метода класса.

А теперь самое интересное: что будет, если попытаться воспользоваться этим в деструкторе? Модернизируем код так, чтобы при деструкции наши питомцы прощались, кто как умеет:

using std :: cout ;
using std :: endl ;

class Cat
<
public :
virtual

cout << «Ordinary Cat: » ; cats [ 0 ] — > askForFood ( ) ;
cout << «Cheshire Cat: » ; cats [ 1 ] — > askForFood ( ) ;

delete cats [ 0 ] ; delete cats [ 1 ] ;
return EXIT_SUCCESS ;
>

Можно ожидать, что, как и в случае с вызовом метода speak(), будет выполнено позднее связывание, однако это не так:

Читать:
Как вытащить клемму из фишки

Почему? Да потому что при вызове виртуальных методов из деструктора компилятор использует не позднее, а раннее связывание. Если подумать, зачем он делает именно так, всё становится очевидным: нужно просто рассмотреть порядок конструирования и разрушения объектов. Все помнят, что конструирование объекта происходит, начиная с базового класса, а разрушение идет в строго обратном порядке. Таким образом, когда мы создаем объект типа CheshireCat, порядок вызовов конструкторов/деструкторов будет таким:

Если же мы захотим внутри деструктора

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

Мораль: если в вашей голове витают помыслы выделить какой-то алгоритм «зачистки» в отдельный метод, переопределяемый в производных классах, а затем виртуально вызывать его в деструкторе, у вас ничего не выйдет.

18.4 – Виртуальные деструкторы, виртуальное присваивание и игнорирование виртуализации

Хотя C++ предоставляет для ваших классов деструктор по умолчанию, если вы не предоставляете его самостоятельно, иногда бывает так, что вам нужно предоставить свой собственный деструктор (особенно, если классу необходимо освободить память). Если вы имеете дело с наследованием, то всегда должны делать свои деструкторы виртуальными. Рассмотрим следующий пример:

Примечание. Если вы попытаетесь скомпилировать приведенный выше пример, ваш компилятор может предупредить вас о не виртуальном деструкторе (что для этого примера сделано специально). Для выполнения компиляции может потребоваться отключить флаг компилятора, который рассматривает предупреждения как ошибки.

Поскольку base является указателем Base , при удалении base программа проверяет, является ли деструктор Base виртуальным. Поскольку это не так, предполагается, что нужно только вызвать деструктор Base . И мы убеждаемся в этом, поскольку в приведенном выше примере печатается:

Однако на самом деле мы хотим, чтобы функция удаления вызывала деструктор Derived (который, в свою очередь, вызывает деструктор Base ), иначе m_array не будет удален. Мы можем сделать это, сделав деструктор Base виртуальным:

Теперь эта программа дает следующий результат:

Правило

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

Как и в случае с обычными виртуальными функциями-членами, если функция базового класса является виртуальной, все производные переопределения будут считаться виртуальными независимо от того, указаны ли они как таковые. Нет необходимости создавать пустой деструктор производного класса только для того, чтобы пометить его как виртуальный.

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

Виртуальное присваивание

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

Игнорирование виртуализации

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

Могут быть случаи, когда вы хотите, чтобы указатель Base на объект Derived вызывал Base::getName() вместо Derived::getName() . Для этого просто используйте оператор разрешения области видимости:

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

Должны ли мы делать все деструкторы виртуальными?

Это частый вопрос, который задают начинающие программисты. Как отмечено в примере выше, если деструктор базового класса не помечен как виртуальный, то программа подвергается риску утечки памяти, если программист позже удалит указатель базового класса, указывающий на объект производного класса. Один из способов избежать этого – пометить все ваши деструкторы как виртуальные. Но должны ли вы это делать?

Легко сказать «да», и в дальнейшем вы сможете использовать любой класс в качестве базового, но это приведет к снижению производительности (к каждому экземпляру вашего класса добавляется виртуальный указатель). Таким образом, вы должны найти компромисс между этими затратами со своими намерениями.

Традиционная мудрость (как первоначально была высказана Хербом Саттером, уважаемым гуру C++) предлагала избегать ситуации утечки памяти, связанной с невиртуальным деструктором, следующим образом: «Деструктор базового класса должен быть либо открытым и виртуальным, либо защищенным и невиртуальным». Класс с защищенным деструктором нельзя удалить с помощью указателя, что предотвращает случайное удаление объекта производного класса с помощью указателя базового класса, когда базовый класс содержит невиртуальный деструктор. К сожалению, это также означает, что и базовый класс нельзя удалить с помощью указателя базового класса, что, по сути, означает, что объект этого класса не может быть динамически размещен или удален, кроме как производным классом. Это также исключает для таких классов использование умных указателей (таких как std::unique_ptr и std::shared_ptr ), что ограничивает полезность этого правила (мы рассмотрим умные указатели в следующей главе). Это также означает, что объект базового класса не может быть размещен в стеке. Это довольно жесткий набор ограничений.

Теперь, когда в язык был введен спецификатор final , наши рекомендации заключаются в следующем:

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

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

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

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

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

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

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

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

Related Posts