虚拟类的每个对象都有一个指向vtable的指针吗?
或者只有具有虚函数的基类对象具有它?
vtable存放在哪里?进程的代码部分或数据部分?
具有虚方法的所有类都将具有由该类的所有对象共享的单个vtable.
每个对象实例都有一个指向该vtable的指针(这就是找到vtable的方式),通常称为vptr.编译器隐式生成用于在构造函数中初始化vptr的代码.
请注意,这些都不是C++语言强制要求的 - 如果需要,实现可以通过其他方式处理虚拟调度.但是,这是我熟悉的每个编译器都使用的实现.Stan Lippman的书"Inside the C++ Object Model"描述了它的工作原理.
像其他人说的那样,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-hierarchy打印)
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)当然会指向Derived1类的v-table,它的函数bark只有一个条目指向Derived1的版本.Derived2的vptr指向Derived2的vtable,它有两个条目.另一个是由它添加的新方法,微笑.它重复了Base :: bark的条目,当然它将指向Base的函数版本,因为它是它的最衍生版本.
在完成一些优化(构造函数内联,......)之后,我还抛弃了由GCC生成的树,并使用-fdump-tree优化.输出使用GCC的中端语言GIMPL
,它是前端独立的,缩进为一些类似C的块结构:
;; Function virtual void Base::bark() (_ZN4Base4barkEv) virtual void Base::bark() (this) {: return; } ;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv) virtual void Derived1::bark() (this) { : return; } ;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv) virtual void Derived2::smile() (this) { : return; } ;; Function int main() (main) int main() () { void * D.1757; struct Derived2 * D.1734; void * D.1756; struct Derived1 * D.1693; : 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) {: OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call]; return; }
OBJ_TYPE_REF
是一个很好的打印的GIMPL构造(它gcc/tree.def
在gcc SVN源代码中记录)
OBJ_TYPE_REF(; -> )
它的含义是:*b->_vptr.Base
在对象上使用表达式b
,并存储前端(c ++)特定值0
(它是vtable中的索引).最后,它b
作为"这个"论点传递.我们会调用一个出现在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插槽).因此,无论在该位置出现什么都被调用.它还将调用标记为尾调用,因为它是我们doit
函数中的最后一个语句.