Ответ 1
Для GCC применимо следующее, но также может быть истинным для используемого компилятора. Все это зависит от реализации и не регулируется стандартом С++. Однако GCC пишет собственный бинарный стандартный документ, Itanium ABI.
Я попытался объяснить основные понятия о том, как виртуальные таблицы изложены более простыми словами как часть статьи о производительности виртуальных функций в С++, которые могут оказаться полезными. Вот ответы на ваши вопросы:
-
Более правильным способом отображения внутреннего представления объекта является:
| 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).
Класс также нуждается в наборе конструктивных vtables, чтобы правильно распределять виртуальные функции при построении баз сложного объекта. Вы можете прочитать далее стандарт, который я связал.
-
Вот пример. Предположим, что
C
наследует отA
иB
, каждый класс, определяющийvirtual void func()
, а такжеA
,B
илиC
виртуальную функцию, относящуюся к ее имени.C
будет иметь группу vtable из двух vtables. Он будет делиться одним vtable с помощьюA
(vtable, где собственные функции текущего класса go называются "primary" ), и будет добавлена таблица vtable для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
перед каждой виртуальной таблицей в группе, и вы будете иметь приблизительную оценку того, как данные лежат внутри объекта. Вы можете прочитать об этом в соответствующем разделе двоичного стандарта GCC. -
Виртуальные базы (некоторые из них) выложены в конце группы vtable. Это делается потому, что каждый класс должен иметь только одну виртуальную базу, и если они смешиваются с "обычными" vtables, то компилятор не может повторно использовать части сконструированных vtables для создания производных классов. Это приведет к выделению ненужных смещений и снижению производительности.
Из-за такого размещения виртуальные базы также вводят в свои vtables дополнительные элементы:
vcall
offset (чтобы получить адрес конечного переопределения при переходе от указателя к виртуальной базе внутри полного объекта к началу класса который переопределяет виртуальную функцию) для каждой виртуальной функции, определенной там. Также каждая виртуальная база добавляет смещенияvbase
, которые вставляются в vtable производного класса; они позволяют найти, где начинаются данные виртуальной базы (ее нельзя предварительно скомпилировать, поскольку фактический адрес зависит от иерархии: виртуальные базы находятся в конце объекта, а сдвиг от начала варьируется в зависимости от того, сколько не виртуальных классы, которые наследует текущий класс.).
Уоф, надеюсь, я не представил много ненужной сложности. В любом случае вы можете обратиться к исходному стандарту или к любому документу своего собственного компилятора.