Двигаемся поэтапно, с самого начала.
1. Никаких виртуальных функций (пока что)
Используем struct вместо class чтобы каждый раз не писать public. В нашем примере не будет никаких приватных членов класса, а в остальном все тоже самое:
|
|
struct Parent { void f() {} // (1) }; struct Child : public Parent { void f() {} // (2) }; |
Функция f() переопределена в дочернем классе Child (overriding, не путать с overloading). Вызов функций:
|
|
Parent* p = new Parent; p->f(); // будет вызвана (1) Child* p = new Child; p->f(); // будет вызвана (2) |
Пока никаких неожиданностей нет — все работает как должно.
Замечу, что если в дочернем классе не будет функции (2), то будет вызвана функция (1) родительского класса. Это происходит потому, что Child хранит гены родителя Parent и в силу наследования имеет доступ ко всем полям родительского класса. Но это так, к слову.
2. Все еще никаких виртуальных функций
Сделаем странную вещь — присвоим указателю на объект родительского класса объект производного класса и поразмышляем над тем, что получилось.
|
|
Parent* p = new Child; p->f(); // будет вызвана (1) !!! |
Указатель на Parent, а объект Child… нет ли здесь несоответствия или противоречия?
Рассуждаем так. Указатель на объект класса — это не просто адрес в памяти. Когда мы имеем дело с классами, фактически имеем набор указателей на все поля и функции класса — это если упростить (а упрощать мы любим). Выше я говорил, что Child хранит «гены» родителя Parent, в нашем случае — функцию (1) родительского класса. Поэтому указатель на Parent находит в Child то, про что он знает — отсылку на свою собственную функцию (1).
При этом родительский класс не знает абсолютно ничего про остальные поля и функции, которые эксклюзивно находятся в Child. И если последний содержит например функцию g(), то компиляция p->g() даст ошибку.
В ситуации наоборот
|
|
Child* p = new Parent; // ошибка компиляции |
Указатель захочет сослаться на поля, которые есть только в Child, но в объекте Parent их быть не может!
3. Подозрительная конструкция
Поехали дальше. Возникает закономерный вопрос: зачем вообще нужна такая конструкция
когда все прекрасно работает и без нее? Следствие, которое ведут Колобки, знает: встретили такую запись — значит где-то поблизости виртуальная функция (и может быть не одна).
И наоборот — видите объявление виртуальной функции, ищите в коде конструкцию подобного вида: она обязательно будет, потому что без нее объявлять функцию виртуальной нет смысла.
Поэтому смотрим пока на то что «нет смысла», а потом переходим к заключительному полноценному примеру.
4. Бессмысленная виртуальная функция
|
|
struct Parent { virtual void f() {} // (1) }; struct Child : public Parent { void f() override {} // (2) }; |
Теперь функция f() стала виртуальной. Заведите хорошую привычку дописывать override в дочерних классах, чтобы во-первых было понятно что функция виртуальная (это здесь все видно, а если вы копаетесь в многочисленных листингах?), и во-вторых это страхует вас от ошибки в сигнатуре функции — без override компилятор все пропустит, но будет считать f() в дочернем классе уже не виртуальной, а с override — предупредит.
Повторяем наш эксперимент с самого начала.
|
|
Parent* p = new Parent; p->f(); // будет вызвана (1) Child* p = new Child; p->f(); // будет вызвана (2) |
Вы заметили какую — нибудь разницу по сравнению с первым сценарием без виртуальных функций? И я тоже нет.
Давайте быстро забудем про это бессмысленное применение virtual и перейдем к нашей сакраментальной конструкции, и тут она уже заиграет в полную силу.
5. Складываем все вместе
Объявление класса с виртуальными функциями:
|
|
struct Parent { virtual void f() {} // (1) }; struct Child : public Parent { void f() override {} // (2) }; |
Действие:
|
|
Parent* p = new Child; p->f(); // будет вызвана (2) !!! |
У меня для вас новость: объект родительского класса каким-то образом узнал про функцию в дочернем классе и вызвал именно ее. Как он это сделал?
Сейчас самое главное — не погрузиться в дебри новых понятий и не потерять прозрачность повествования. Новое понятие — это таблица виртуальных функций vtable класса Parent, которая содержит указатели на виртуальные функции. Отныне все вызовы виртуальных функций будут происходить не через указатель класса Parent* p, а через посредника — таблицу vtable.
Принципиально важным является то, что содержимое этой таблицы не задается жестко во время компиляции, а может меняться по ходу выполнения программы (это то, что вас на собеседовании спрашивали про позднее связывание или dynamic binding). Если мы поймем, как и когда эта таблица заполняется и меняется, мы поймем все про виртуальные функции. Поехали!
6. Gdb, vtbl
Ехать будем с помощью отладчика gdb — это наиболее удобный способ посмотреть расположение vtable и виртуальных функций в памяти. Поменяем наш пример, чтобы с отладчиком было удобнее работать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
#include <iostream> using namespace std; class Base { public: virtual void base_and_derived(void) { cout << "Base::f" << endl; } virtual void base(void) { cout << "Base::fonly" << endl; } }; class Derived : public Base { public: void base_and_derived(void) override { cout << "Derived::f" << endl; } virtual void derived(void) { cout << "Derived::v" << endl; } }; int main (int argh, char *argv[]) { Base* b = new Base(); Derived* d = new Derived(); b = d; return 0; } |
Компилируемся, запускаем отладку:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
(gdb) b main Breakpoint 1 at 0x11dd: file faddr.cpp, line 21. (gdb) run Starting program: /home/***/faddr [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Breakpoint 1, main (argh=1, argv=0x7fffffffdfc8) at faddr.cpp:21 21 b = new Base(); (gdb) n 23 d = new Derived(); (gdb) n 24 b = d; (gdb) info vtbl b vtable for 'Base' @ 0x555555557d50 (subobject @ 0x55555556aeb0): [0]: 0x5555555552a0 <Base::base_and_derived()> [1]: 0x5555555552de <Base::base()> (gdb) info vtbl d vtable for 'Derived' @ 0x555555557d28 (subobject @ 0x55555556aed0): [0]: 0x55555555531c <Derived::base_and_derived()> [1]: 0x5555555552de <Base::base()> [2]: 0x55555555535a <Derived::derived()> |
Замечу что b = d еще не выполнялось: мы остановились до этой строчки.
Для успокоения души проверим адреса, которые нам выдал отладчик, например для b:
|
|
(gdb) x /2xg 0x555555557d50 0x555555557d50 <_ZTV4Base+16>: 0x00005555555552a0 0x00005555555552de |
Все в порядке, vtable содержит адреса Base::base_and_derived() и Base::base() (доверяй, но проверяй!)
Теперь посмотрим на таблицы, на какие виртуальные функции они указывают. С классом Base все понятно, это пара собственных виртуальных функций base_and_derived(), base(). С дочерним классом Derived немного интереснее: его таблица содержит переопределенную виртуальную функцию базового класса base_and_derived(), собственно виртуальную функцию base() которая принадлежит базовому классу (родитель!) и собственная виртуальная derived().
Само собой, адреса виртуальных таблиц 0x555555557d50 и 0x555555557d28 различаются, поскольку у каждого класса (не экземпляра класса!) своя виртуальная таблица. Это замечание пригодится нам прямо сейчас.
Продолжаем программу и выполняем наше легендарное присвоение указателя производного класса указателю базового класса:
|
|
(gdb) n 26 return 0; (gdb) info vtbl b vtable for 'Base' @ 0x555555557d28 (subobject @ 0x55555556aed0): [0]: 0x55555555531c <Derived::base_and_derived()> [1]: 0x5555555552de <Base::base()> (gdb) info vtbl d vtable for 'Derived' @ 0x555555557d28 (subobject @ 0x55555556aed0): [0]: 0x55555555531c <Derived::base_and_derived()> [1]: 0x5555555552de <Base::base()> [2]: 0x55555555535a <Derived::derived()> |
Произошли существенные изменения, и главное из них — мы фактически потеряли виртуальную таблицу базового класса (что еще можно было ожидать от присваивания?) Точнее, базовый и производный класс сейчас содержат только одну виртуальную таблицу расположенную по адресу 0x555555557d28, которая по сути таблица класса Derived оставленная без изменений —.
Поскольку класс Base теперь пользуется виртуальной таблицей класса Derived, предыдущая запись
0x5555555552a0 <Base::base_and_derived()>
Поменялась на запись
0x5555555552a0 <Base::base_and_derived()>
какой она и была для Derived раньше.
Это означает: все вызовы виртуальной функции base_and_derived выполненные с помощью указателя b->base_and_derived() приведут не к вызову виртуальной функции базового класса (как можно было ожидать, ведь b — указатель на Base), а к вызову виртуальной функции произвольного класса.
Что и требовалось доказать.
7. Послесловие
Вот и все. Приведу пример, когда это бывает нужно в реальной жизни. Например, клиент знает только про класс Interface и пользуется им:
|
|
struct Interface { virtual int api() {} // client interface }; // то что ниже - клиенту знать не обязательно struct Implementation_1 : public Parent { int api() override {} // api version 1 }; struct Implementation_2 : public Parent { int api() override {} // api version 2 }; |
У нас есть возможность подсовывать различные варианты реализации интерфейса, оставляя клиента в неведении:
|
|
Interface* if; if = new Implementation_1; if->api(); // будет вызвана версия 1 api if = new Implementation_2; if->api(); // будет вызвана версия 2 api |
Этот пример можно воспринимать по другому: например, Implementation_1 это боевая реализация, а Implementation_2 это mock заглушка для тестирования. Или Interface это класс прокси или адаптера. В общем все шаблоны C++ которые только существуют базируются на этой записи:
и это все, что вам нужно знать про полиморфизм в C++.
Last comments