Я попытался объяснить основные понятия о том, как виртуальные таблицы представлены в более простых словах, как часть моей статьи о производительности виртуальных функций в C ++ , которая может оказаться полезной. Вот ответы на ваши вопросы:
Более правильный способ изобразить внутреннее представление объекта:
| vptr | ======= | ======= | <-- your object
|----A----| |
|---------B---------|
B
содержит его базовый класс A
, он просто добавляет пару своих собственных членов после его окончания.
Приведение от B*
к A*
действительно ничего не делает, возвращает тот же указатель, а vptr
остается тем же. Но, в двух словах, виртуальные функции не всегда вызываются через vtable . Иногда они вызываются как другие функции.
Вот более подробное объяснение. Следует различать два способа вызова функции-члена:
A a, *aptr;
a.func(); // the call to A::func() is precompiled!
aptr->A::func(); // ditto
aptr->func(); // calls virtual function through vtable.
// It may be a call to A::func() or B::func().
Дело в том, что во время компиляции известно , как будет вызываться функция: через vtable или просто будет обычным вызовом. И дело в том, что тип выражения приведения известен во время компиляции , и поэтому компилятор выбирает правильную функцию во время компиляции.
B b, *bptr;
static_cast<A>(b)::func(); //calls A::func, because the type
// of static_cast<A>(b) is A!
В этом случае он даже не заглядывает внутрь vtable!
Как правило, нет. Класс может иметь несколько vtables, если он наследует от нескольких баз, каждая из которых имеет свою собственную vtable. Такой набор виртуальных таблиц образует «группу виртуальных таблиц» (см. П. 3).
Классу также необходим набор конструкционных таблиц, чтобы правильно распределять виртуальные функции при построении баз сложного объекта. Вы можете прочитать далее в стандарт, который я связал .
Вот пример. Предположим, что C
наследуется от A
и B
, каждый класс определяет virtual void func()
, а также виртуальные функции a
, b
или c
, соответствующие его имени.
У C
будет группа vtable из двух vtables. Он будет использовать одну виртуальную таблицу с A
(виртуальная таблица, в которую входят собственные функции текущего класса, называется «основной»), и к ней добавляется виртуальная таблица для B
:
| C::func() | a() | c() || C::func() | b() |
|---- vtable for A ----| |---- vtable for B ----|
|--- "primary virtual table" --||- "secondary vtable" -|
|-------------- virtual table group for C -------------|
Представление объекта в памяти будет выглядеть почти так же, как выглядит его vtable. Просто добавьте vptr
перед каждой vtable в группе, и вы получите приблизительную оценку того, как данные расположены внутри объекта. Вы можете прочитать об этом в соответствующем разделе двоичного стандарта GCC.
Виртуальные базы (некоторые из них) располагаются в конце группы vtable. Это сделано потому, что у каждого класса должна быть только одна виртуальная база, и если они смешаны с «обычными» виртуальными таблицами, то компилятор не сможет повторно использовать части созданных виртуальных таблиц для создания частей производных классов. Это приведет к вычислению ненужных смещений и уменьшит производительность.
Из-за такого размещения виртуальные базы также вводят в свои vtables дополнительные элементы: vcall
offset (для получения адреса конечной переопределения при переходе от указателя к виртуальной базе внутри законченного объекта в начале класса). который переопределяет виртуальную функцию) для каждой виртуальной функции, определенной там. Также каждая виртуальная база добавляет vbase
смещений, которые вставляются в vtable производного класса; они позволяют определить, где начинаются данные виртуальной базы (их нельзя предварительно скомпилировать, поскольку фактический адрес зависит от иерархии: виртуальные базы находятся в конце объекта, а смещение от начала зависит от количества не виртуальных классы, которые наследует текущий класс.).
Гав, надеюсь, я не привнес много лишней сложности. В любом случае вы можете ссылаться на исходный стандарт или на любой документ собственного компилятора.