Здравствуйте! На этом уроке мы разберем, как связаны конструкторы и деструкторы с виртуальными методами. Попробуем пристально посмотреть сейчас на "Vpointer", который находится в начале классов. Попробуем его напечатать, просто напечатать. Для этого напишем вспомогательную функцию "PrintVpointer", которая принимает указатель на любой объект, а потом с помощью приведения типа приводит первые восемь байтов к типу "size_t" для того, чтобы потом его распечатать. Такое приведение типов мы можем уже сделать с помощью знакомого нам приведения "reinterpret_cast". И потом просто его печатаем. Давайте попробуем это сделать. Создадим по паре экземпляров каждого из классов — два животных, два млекопитающих, две собаки, две кошки. Что сделаем дальше? Дальше вызовем функцию "PrintVpointer", чтобы посмотреть, чему равно значение указателя в каждом из этих объектов. Для животных, для млекопитающих. Запускаем. Что мы видим? Что у нас все "Vpointer" попарно равны. То есть "Vpointer" для одного класса действительно указывают на один и тот же адрес. Это подтверждает то, что мы узнали на прошлом занятии — что "Vtable" для класса существует один, и "Vpointer" разных экземпляров этого класса ссылаются на одну и ту же таблицу, адреса совпадают. Что мы сделаем дальше? Дальше мы, имея эту функцию, поэкспериментируем с конструкторами и деструкторами. Давайте внутри каждого конструктора и деструктора вызывать этот метод и смотреть, что происходит. Код написан, и мы в качестве указателя на объект всегда передаем "this", чтобы посмотреть, что хранится в первых восьми байтах каждого экземпляра классов. Допустим, берем пока наш экспериментальный код, запускаем. Так, у нас получается тут очень много лишней печати, потому что у нас и собака, и кошка создается. Давайте пока кошку не будем создавать. Смотрим только на собаку. Так что происходит? У нас вызываются последовательно три конструктора — "Animal", "Mammal" и "Dog". И в каждом из трех этих конструкторов у нас меняется указатель "Vpointer". Посмотрим на последовательность вызовов деструкторов. В деструкторе "Animal" вызывается тот же самый "Vpointer", что и в конструкторе. Но мы здесь вообще не видим вызовов деструкторов других классов. И это очень важное замечание, очень важное наблюдение. Почему так происходит? Потому что мы не объявили деструктор виртуальным, а это нужно было сделать. Потому что иначе они никак не вызовутся. Давайте исправим нашу ошибку. Найдем деструктор класса "Animal" и добавим ключевое слово "virtual". Отлично, теперь все работает. У нас деструкторы вызываются в обратном порядке, и "Vpointer" внутри деструкторов равны соответствующим "Vpointer" в конструкторах. То есть используется одна и та же таблица виртуальных функций на эти вызовы. Перейдем к итогам. На этом уроке мы рассмотрели, что происходит с указателями на "Vtable" при создании и удалении объекта. Сначала мы убедились, что для каждого класса с виртуальными методами существует одна и только одна таблица виртуальных функций. Созданием всех таблиц занимается компилятор. Вся информация известна на этапе компиляции. При создании объекта каждый конструктор, от базового типа к самому дочернему, переключает "Vpointer" на "Vtable" своего типа. Таким образом, после завершения конструирования объекта "Vpointer" будет указывать на таблицу виртуальных функций самого дочернего класса. Деструктуры, соответственно, переключают "Vpointer" в обратном порядке. И самое важное следствие из этого урока: очень важно не забывать объявлять деструктор виртуальным в базовом классе. Это необходимо делать практически всегда. За редким исключением, лишь когда вы явно понимаете, что вам это не нужно. Типичный пример, показывающий необходимость этого, — когда потомок выделяет память, а деструктор родителя не имеет возможности ее очистить. Поэтому необходимо знать о деструкторе потомка.