Виртуальный деструктор
В предыдущей статье в комментарии к примеру было написано, что деструктор полиморфного класса обязательно должен быть виртуальным. Зачем же? Погнали разбираться!
Объект каждого класса обладает своим жизненным циклом: начало, счастливая жизнь и конец. С началом ассоциирован - конструктор, а с концом - деструктор, о котором и пойдет речь дальше. Он вызывается автоматически, когда объект удаляется вручную или автоматически. Это даёт возможность корректно завершить работу, которую выполнял объект. Например, освободить выделенную в конструкторе память, закрыть сокет. Это позволяет поддерживать систему в валидном состоянии на протяжении всей работы программы. Это счастливый конец!
Условно, если вы поработали в мастерской, значит, уходя из неё, надо всё за собой убрать и разложить по полочкам. Иначе следующий мастер там просто не найдет нужный инструмент, запнется о мусор и еще что-нибудь испортит. Повторим это с десяток раз и можно сжигать мастерскую 😃. Кажется, что сжигать мастерскую — это перебор 🤭, но именно так и поступит система: прибьет её из-за исчерпания памяти (Out Of Memory). Это грустный конец...
Вернёмся к динамическому полиморфизму. Давайте свяжем два наблюдения:
1) Зачастую, наследники полиморфных классов могут владеть ресурсом, который обязаны вернуть системе (например, память на куче).
2) Зачастую, взаимодействие происходит через указатель на родительский класс.
Из П.1 следует, что у наследника должен быть вызван деструктор, в котором происходит возврат ресурса системе.
Из П.2 следует, что динамический тип объекта может отличаться от типа указателя.
Из этого следует, что корректное удаление объекта подразумевает вызов деструктора класса наследника. И вот как его вызвать, если тип указателя - родительский? Например тут:
Пишу тут
Есть простое встроенное решение 😊 Отмечайте деструктор родительского класса виртуальным! Пример:
Вызов виртуального деструктора приведёт к вызову цепочки деструкторов у всех наследников от родительского до динамического типа:
Если в иерархии классов деструктор не был отмечен виртуальным, то будет вызван только деструктор класса, который является типом указателя. Деструктор наследника не будет вызван вообще, следовательно, ресурс будет упущен. Добавлю живой пример, в котором количество выделяемых ресурсов не совпадает с освобождаемым. Как-нибудь напишем, как это дело проверять по-нормальному 😉
Пока деструктор останется невиртуальным, у компилятора просто нет указания, что надо заботиться о чем-то. Ну вдруг у вас умышленно очень хитрое поведение у программы?
Есть еще один тонкий момент. Кажется, что если полиморфное семейство не выделяет никаких ресурсов, то и проблем не будет. Но это пока! Пройдет время, код эволюционирует, появятся такие ресурсы и вот тогда что-то может да потечь. Короче, это чеховское ружьё 😉
Помочь избежать этих проблем поможет, как всегда, предупреждение:
#cppcore
В предыдущей статье в комментарии к примеру было написано, что деструктор полиморфного класса обязательно должен быть виртуальным. Зачем же? Погнали разбираться!
Объект каждого класса обладает своим жизненным циклом: начало, счастливая жизнь и конец. С началом ассоциирован - конструктор, а с концом - деструктор, о котором и пойдет речь дальше. Он вызывается автоматически, когда объект удаляется вручную или автоматически. Это даёт возможность корректно завершить работу, которую выполнял объект. Например, освободить выделенную в конструкторе память, закрыть сокет. Это позволяет поддерживать систему в валидном состоянии на протяжении всей работы программы. Это счастливый конец!
Условно, если вы поработали в мастерской, значит, уходя из неё, надо всё за собой убрать и разложить по полочкам. Иначе следующий мастер там просто не найдет нужный инструмент, запнется о мусор и еще что-нибудь испортит. Повторим это с десяток раз и можно сжигать мастерскую 😃. Кажется, что сжигать мастерскую — это перебор 🤭, но именно так и поступит система: прибьет её из-за исчерпания памяти (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