Грокаем C++
4.57K subscribers
4 photos
3 files
244 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Чат: https://t.me/+qJ8-vWd97nExZGIy
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Виртуальный деструктор

В предыдущей статье в комментарии к примеру было написано, что деструктор полиморфного класса обязательно должен быть виртуальным. Зачем же? Погнали разбираться!

Объект каждого класса обладает своим жизненным циклом: начало, счастливая жизнь и конец. С началом ассоциирован - конструктор, а с концом - деструктор, о котором и пойдет речь дальше. Он вызывается автоматически, когда объект удаляется вручную или автоматически. Это даёт возможность корректно завершить работу, которую выполнял объект. Например, освободить выделенную в конструкторе память, закрыть сокет. Это позволяет поддерживать систему в валидном состоянии на протяжении всей работы программы. Это счастливый конец!

Условно, если вы поработали в мастерской, значит, уходя из неё, надо всё за собой убрать и разложить по полочкам. Иначе следующий мастер там просто не найдет нужный инструмент, запнется о мусор и еще что-нибудь испортит. Повторим это с десяток раз и можно сжигать мастерскую 😃. Кажется, что сжигать мастерскую — это перебор 🤭, но именно так и поступит система: прибьет её из-за исчерпания памяти (Out Of Memory). Это грустный конец...

Вернёмся к динамическому полиморфизму. Давайте свяжем два наблюдения:
1) Зачастую, наследники полиморфных классов могут владеть ресурсом, который обязаны вернуть системе (например, память на куче).
2) Зачастую, взаимодействие происходит через указатель на родительский класс.

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

Из этого следует, что корректное удаление объекта подразумевает вызов деструктора класса наследника. И вот как его вызвать, если тип указателя - родительский? Например тут:
Parent *data = new Child();
...
delete data;

Пишу тут new и delete в ознакомительных целях. Используйте умные указатели: unique_ptr, shared_ptr.

Есть простое встроенное решение 😊 Отмечайте деструктор родительского класса виртуальным! Пример:
struct Parent
{
...
   virtual ~Parent() {...}
   ...
};

Вызов виртуального деструктора приведёт к вызову цепочки деструкторов у всех наследников от родительского до динамического типа:
... -> ~Child_2() -> ~Child_1() -> ~Parent();

Если в иерархии классов деструктор не был отмечен виртуальным, то будет вызван только деструктор класса, который является типом указателя. Деструктор наследника не будет вызван вообще, следовательно, ресурс будет упущен. Добавлю живой пример, в котором количество выделяемых ресурсов не совпадает с освобождаемым. Как-нибудь напишем, как это дело проверять по-нормальному 😉

Пока деструктор останется невиртуальным, у компилятора просто нет указания, что надо заботиться о чем-то. Ну вдруг у вас умышленно очень хитрое поведение у программы?

Есть еще один тонкий момент. Кажется, что если полиморфное семейство не выделяет никаких ресурсов, то и проблем не будет. Но это пока! Пройдет время, код эволюционирует, появятся такие ресурсы и вот тогда что-то может да потечь. Короче, это чеховское ружьё 😉

Помочь избежать этих проблем поможет, как всегда, предупреждение:
-Wdelete-non-virtual-dtor

#cppcore