g++对每个类的指针或引用对象,如果是其类声明中虚函数,使用位于其内存空间首地址上的vptr寻找找到vtbl进而得到函数地址。如果是父类声明而子类未覆盖的虚函数,使用对应父类的vptr进行寻址。
先来验证一下,使用 objdump -S 得到 b1.f() 的汇编指令:
Assembly (x86)
b1.f(); 8048734: 8b 44 24 24 mov 0x24(%esp),%eax # 得到Base1对象的地址 8048738: 8b 00 mov (%eax),%eax # 对对象首地址上的vptr进行解引用,得到vtbl地址 804873a: 8b 10 mov (%eax),%edx # 解引用vtbl上第一个虚函数的地址 804873c: 8b 44 24 24 mov 0x24(%esp),%eax 8048740: 89 04 24 mov %eax,(%esp) 8048743: ff d2 call *%edx # 调用函数其过程和我们的分析完全一致,聪明的你可能发现了,b2怎么办呢?Derived类的实例内存首地址上的vptr并不是Base2类的啊!答案实际上是因为g++���引用赋值语句 Base2 &b2 = ins 上动了手脚:
Assembly (x86)
Derived ins; 804870d: 8d 44 24 1c lea 0x1c(%esp),%eax 8048711: 89 04 24 mov %eax,(%esp) 8048714: e8 c3 01 00 00 call 80488dc <_ZN7DerivedC1Ev> Base1 &b1 = ins; 8048719: 8d 44 24 1c lea 0x1c(%esp),%eax 804871d: 89 44 24 24 mov %eax,0x24(%esp) Base2 &b2 = ins; 8048721: 8d 44 24 1c lea 0x1c(%esp),%eax # 获得ins实例地址 8048725: 83 c0 04 add $0x4,%eax # 添加一个指针的偏移量 8048728: 89 44 24 28 mov %eax,0x28(%esp) # 初始化引用 Derived &d = ins; 804872c: 8d 44 24 1c lea 0x1c(%esp),%eax 8048730: 89 44 24 2c mov %eax,0x2c(%esp)虽然是指向同一个实例的引用,根据引用类型的不同,g++编译器会为不同的引用赋予不同的地址。例如b2就获得一个指针的偏移量,因此才保证了vptr的正确性。
PS:我们顺便也证明了C++中的引用的真实身份就是指针…
接下来进入第二个问题:
vtbl在何时被创建?vptr又是在何时被初始化?既然我们已经知道了g++是如何通过vptr和vtbl来实现虚函数魔法的,那么vptr和vtbl又是在什么时候被创建的呢?