Как сказал кто-то еще, стандарт C ++ не предписывает таблицу виртуальных методов, но позволяет использовать ее. Я провел свои тесты, используя gcc, этот код и один из самых простых возможных сценариев:
class Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived1 : public Base {
public:
virtual void bark() { }
int dont_do_ebo;
};
class Derived2 : public Base {
public:
virtual void smile() { }
int dont_do_ebo;
};
void use(Base* );
int main() {
Base * b = new Derived1;
use(b);
Base * b1 = new Derived2;
use(b1);
}
Добавлены члены-данные, чтобы компилятор не давал базовому классу нулевой размер (он известен как оптимизация пустого базового класса). Это макет, который выбрал GCC: (печать с использованием -fdump-class -ierarchy)
Vtable for Base
Base::_ZTV4Base: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI4Base)
8 Base::bark
Class Base
size=8 align=4
base size=8 base align=4
Base (0xb7b578e8) 0
vptr=((& Base::_ZTV4Base) + 8u)
Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived1)
8 Derived1::bark
Class Derived1
size=12 align=4
base size=12 base align=4
Derived1 (0xb7ad6400) 0
vptr=((& Derived1::_ZTV8Derived1) + 8u)
Base (0xb7b57ac8) 0
primary-for Derived1 (0xb7ad6400)
Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0 (int (*)(...))0
4 (int (*)(...))(& _ZTI8Derived2)
8 Base::bark
12 Derived2::smile
Class Derived2
size=12 align=4
base size=12 base align=4
Derived2 (0xb7ad64c0) 0
vptr=((& Derived2::_ZTV8Derived2) + 8u)
Base (0xb7b57c30) 0
primary-for Derived2 (0xb7ad64c0)
Как видите, у каждого класса есть vtable. Первые две записи являются особенными. Второй указывает на данные RTTI класса. Первый - я знал это, но забыл. Это имеет некоторое применение в более сложных случаях. Итак, как показано на схеме, если у вас есть объект класса Derived1, тогда vptr (v-table-pointer) будет указывать на v-таблицу класса Derived1, которая имеет ровно одну запись для своей функции, указывающую на кору Версия Derived1. Vptr Derived2 указывает на vtable Derived2, который имеет две записи. Другой - новый метод, добавленный им, улыбка. Он повторяет запись для Base :: bark, которая, конечно, будет указывать на версию функции Base, потому что это самая производная версия.
Я также выкинул дерево, сгенерированное GCC после некоторой оптимизации (конструктор встроен, ...), с -fdump-tree-optimized. Выходные данные используют язык среднего уровня GCC GIMPL
, который не зависит от внешнего интерфейса и имеет отступ в некоторую C-подобную блочную структуру:
;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
return;
}
;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
return;
}
;; Function int main() (main)
int main() ()
{
void * D.1757;
struct Derived2 * D.1734;
void * D.1756;
struct Derived1 * D.1693;
<bb 2>:
D.1756 = operator new (12);
D.1693 = (struct Derived1 *) D.1756;
D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
use (&D.1693->D.1671);
D.1757 = operator new (12);
D.1734 = (struct Derived2 *) D.1757;
D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
use (&D.1734->D.1682);
return 0;
}
Как мы можем видеть, это просто установка одного указателя - vptr - который будет указывать на соответствующую vtable, которую мы видели ранее при создании объекта. Я также сбросил ассемблерный код для создания Derived1 и вызова для использования ($ 4 - регистр первого аргумента, $ 2 - регистр возвращаемого значения, $ 0 - всегда-0-регистр) после разборки имен в нем с помощью c++filt
инструмент:)
# 1st arg: 12byte
add $4, $0, 12
# allocate 12byte
jal operator new(unsigned long)
# get ptr to first function in the vtable of Derived1
add $3, $0, vtable for Derived1+8
# store that pointer at offset 0x0 of the object (vptr)
stw $3, $2, 0
# 1st arg is the address of the object
add $4, $0, $2
jal use(Base*)
Что произойдет, если мы захотим позвонить bark
?:
void doit(Base* b) {
b->bark();
}
код GIMPL:
;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
return;
}
OBJ_TYPE_REF
- это конструкция GIMPL, которая довольно напечатана (она задокументирована в gcc/tree.def
в исходном коде gcc SVN)
OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)
Это означает: используйте выражение *b->_vptr.Base
для объекта b
и сохраните определенное значение внешнего интерфейса (c ++) 0
(это индекс в vtable). Наконец, он передает b
в качестве аргумента "this". Если бы мы вызывали функцию, которая появляется во втором индексе в vtable (заметьте, мы не знаем, какой vtable какого типа!), GIMPL выглядел бы так:
OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];
Конечно, здесь снова ассемблерный код (материал стекового фрейма обрезан):
# load vptr into register $2
# (remember $4 is the address of the object,
# doit's first arg)
ldw $2, $4, 0
# load whatever is stored there into register $2
ldw $2, $2, 0
# jump to that address. note that "this" is passed by $4
jalr $2
Помните, что vptr указывает именно на первую функцию. (До этого входа слот RTTI хранился). Итак, все, что появляется в этом слоте, называется. Он также помечает вызов как tail-call, потому что это происходит как последний оператор в нашей функции doit
.