Как достигается эффект переопределения виртуальной функции в дочернем классе в C++
Что проверяют
Понимаете ли вы, как динамическая диспетчеризация устроена под капотом, а не только синтаксис virtual. Следом спрашивают, что будет при вызове виртуальной функции из конструктора и зачем нужен виртуальный деструктор.
Как отвечать
Стандарт описывает только поведение, но основные компиляторы реализуют его одинаково:
- vtable: у каждого полиморфного класса одна статическая таблица с адресами реализаций. В таблице наследника на месте переопределённых функций стоят его адреса, на месте остальных адреса базового класса;
- vptr: скрытый указатель в каждом объекте, обычно в самом начале, 8 байт на 64-битной платформе. Его выставляет конструктор;
- вызов:
b->show()через указатель или ссылку наBaseкомпилируется в «прочитать vptr, взять слот, вызвать по адресу», поэтому выполняется версия реального типа объекта.
Правила переопределения. Сигнатура должна совпадать, включая const и ref-квалификаторы, иначе получится новая функция, которая прячет базовую. Возвращаемый тип может быть ковариантным. override из C++11 заставляет компилятор проверить, что переопределение действительно есть, а final запрещает дальнейшее переопределение и помогает девиртуализировать вызов.
Цена: косвенный вызов и невозможность встроить функцию, пока тип неизвестен при компиляции. Удаление через указатель на базу без виртуального деструктора это неопределённое поведение, а владеть объектом лучше через std::unique_ptr<Base>, а не голый new.
Где спотыкаются
Вызывают виртуальную функцию в конструкторе базового класса и ждут версию наследника. Пока работает конструктор Base, объект считается объектом Base, а вызов чисто виртуальной функции там даёт неопределённое поведение, на практике аварийное завершение.
Передают объект по значению типа Base. Происходит срезка: от наследника остаётся базовая часть, и полиморфизм исчезает.
Объявляют в наследнике перегрузку с тем же именем. Она скрывает все базовые перегрузки, вернуть их можно через using Base::show;.