Набросайте схему vtable и установку vptr для иерархии
Для полиморфной иерархии ниже набросайте концептуальную vtable для каждого класса (какие слоты, к какой функции разрешается каждый слот) и объясните, как скрытый vptr устанавливается в процессе конструирования.
Разберите явно:
- одна статическая vtable на полиморфный класс, один
vptrна экземпляр (не хранение на класс) - каждый конструктор сбрасывает
vptrна vtable своего класса — virtual-вызов внутри конструктора не доходит до производного override - множественное наследование: сколько
vptrнесёт объект производного класса
struct Animal {
virtual void speak() { std::puts("..."); }
virtual void move() { std::puts("move"); }
virtual ~Animal() = default;
};
struct Dog : Animal {
void speak() override { std::puts("woof"); }
// move() not overridden
};
Набросайте схему и объясните поведение vptr.
Каждый полиморфный класс имеет статическую vtable; каждый экземпляр содержит скрытый vptr. Каждый конструктор сбрасывает vptr на vtable своего класса — virtual-вызовы в конструкторах не доходят до производных.
- ✗Думать, что vtable принадлежит экземпляру — она принадлежит классу (статические данные)
- ✗Предполагать, что виртуальное наследование не вводит дополнительную сложность vptr
- ✗Использование reinterpret_cast для прямого чтения vptr — непереносимый ABI-хак
- →Что такое чисто виртуальная функция и как она представлена в vtable?
- →Как множественное наследование влияет на структуру vtable?
Иерархия
struct Animal {
virtual void speak() { std::puts("..."); }
virtual void move() { std::puts("move"); }
virtual ~Animal() = default;
};
struct Dog : Animal {
void speak() override { std::puts("woof"); }
// move() не переопределён
};
Концептуальная структура vtable
vtable Animal:
[0] Animal::~Animal (thunk деструктора)
[1] Animal::speak
[2] Animal::move
vtable Dog:
[0] Dog::~Dog (thunk деструктора)
[1] Dog::speak ← переопределяет Animal::speak
[2] Animal::move ← унаследован, не переопределён
Каждый экземпляр Animal или Dog начинается со скрытого vptr:
Объект Dog в памяти:
+0 vptr → &Dog_vtable
+8 (поля данных Dog, если есть)
Последовательность конструирования
Dog d;
// Шаг 1: выполняется конструктор Animal — vptr указывает на &Animal_vtable
// выполняется тело Animal::Animal()
// Шаг 2: выполняется конструктор Dog — vptr обновляется до &Dog_vtable
// выполняется тело Dog::Dog()
// Результат: d.vptr == &Dog_vtable
Именно поэтому Animal::Animal() при вызове speak() диспетчеризуется к Animal::speak — в этот момент vptr всё ещё указывает на Animal_vtable.
Проверка во время выполнения (непереносимо, только для иллюстрации)
// На большинстве ABI-совместимых реализаций:
void** vptr = *reinterpret_cast<void***>(&d);
// vptr[1] — слот для speak()
// Никогда не делайте этого в производственном коде.
Множественное наследование и vtable
struct A { virtual void f(); };
struct B { virtual void g(); };
struct C : A, B {};
Экземпляры C содержат два vptr (по одному на каждый базовый класс), и расположение выглядит следующим образом:
Объект C:
+0 vptr_A → vtable A-подобъекта C (с C::f)
+8 (данные A)
+N vptr_B → vtable B-подобъекта C (с C::g)
+N+8 (данные B)