Виртуальные функции C++. Следствие ведут Колобки

Двигаемся поэтапно, с самого начала.

1. Никаких виртуальных функций (пока что)

Используем struct вместо class чтобы каждый раз не писать public. В нашем примере не будет никаких приватных членов класса, а в остальном все тоже самое:

Функция f()  переопределена в дочернем классе Child (overriding, не путать с overloading). Вызов функций:

Пока никаких неожиданностей нет — все работает как должно.

Замечу, что если в дочернем классе не будет функции (2), то будет вызвана функция (1) родительского класса. Это происходит потому, что Child хранит гены родителя Parent и в силу наследования имеет доступ ко всем полям родительского класса. Но это так, к слову.

2. Все еще никаких виртуальных функций

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

Указатель на Parent, а объект Child… нет ли здесь несоответствия или противоречия?

Рассуждаем так. Указатель на объект класса — это не просто адрес в памяти. Когда мы имеем дело с классами, фактически имеем набор указателей на все поля и функции класса — это если упростить (а упрощать мы любим). Выше я говорил, что Child хранит «гены» родителя Parent, в нашем случае — функцию (1) родительского класса. Поэтому указатель на Parent находит в Child то, про что он знает — отсылку на свою собственную функцию (1).

При этом родительский класс не знает абсолютно ничего про остальные поля и функции, которые эксклюзивно находятся в Child. И если последний содержит например функцию g(), то компиляция p->g() даст ошибку.

В ситуации наоборот

Указатель захочет сослаться на поля, которые есть только в Child, но в объекте Parent их быть не может!

3. Подозрительная конструкция

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

когда все прекрасно работает и без нее? Следствие, которое ведут Колобки, знает: встретили такую запись — значит где-то поблизости виртуальная функция (и может быть не одна).

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

Поэтому смотрим пока на то что «нет смысла», а потом переходим к заключительному полноценному примеру.

4. Бессмысленная виртуальная функция

Теперь функция f() стала виртуальной. Заведите хорошую привычку дописывать override в дочерних классах, чтобы во-первых было понятно что функция виртуальная (это здесь все видно, а если вы копаетесь в многочисленных листингах?), и во-вторых это страхует вас от ошибки в сигнатуре функции — без override компилятор все пропустит, но будет считать f() в дочернем классе уже не виртуальной, а с override — предупредит.

Повторяем наш эксперимент с самого начала.

Вы заметили какую — нибудь разницу по сравнению с первым сценарием без виртуальных функций? И я тоже нет.

Давайте быстро забудем про это бессмысленное применение virtual и перейдем к нашей сакраментальной конструкции, и тут она уже заиграет в полную силу.

5. Складываем все вместе

Объявление класса с виртуальными функциями:

Действие:

У меня для вас новость: объект родительского класса каким-то образом узнал про функцию в дочернем классе и вызвал именно ее. Как он это сделал?

Сейчас самое главное — не погрузиться в дебри новых понятий и не потерять прозрачность повествования. Новое понятие — это таблица виртуальных функций vtable класса Parent, которая содержит указатели на виртуальные функции. Отныне все вызовы виртуальных функций будут происходить не через указатель класса Parent* p, а через посредника — таблицу vtable.

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

Послесловие

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

У нас есть возможность подсовывать различные варианты реализации интерфейса, оставляя клиента в неведении:

Этот пример можно воспринимать по другому: например, Implementation_1 это боевая реализация, а Implementation_2 это mock заглушка для тестирования. Или Interface это класс прокси или адаптера. В общем все шаблоны C++ которые только существуют базируются на этой записи:

и это все, что вам нужно знать про полиморфизм в C++.

Ответить

Вы можете использовать эти HTML теги

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">