1. 关于虚继承
虚继承可以在菱形继承体系中,防止派生类中有多份重复祖基类内容。如下图所示,如果是常规继承,Class Final中会有两份Class Base的内容。通过虚继承,即Derived1 虚继承自Base, Derived2 也虚继承自Base, 那么Final中将最终保留一份Base部分的内容。
2. 代码示例
#include <iostream>class Base {
public:virtual void show() {std::cout << "Base::show() called" << std::endl;}int a = 1;
};class Derived1 : virtual public Base {
public:void show() override {std::cout << "Derived1::show() called" << std::endl;}int b = 2;
};class Derived2 : virtual public Base {
public:void show() override {std::cout << "Derived2::show() called" << std::endl;}int c = 3;
};class Final : public Derived1, public Derived2 {
public:void show() override {std::cout << "Final::show() called" << std::endl;}int d = 4;
};int main() {std::cout << "Base size: " << sizeof(Base) << std::endl;std::cout << "Derived1 size: " << sizeof(Derived1) << std::endl;std::cout << "Derived2 size: " << sizeof(Derived2) << std::endl;std::cout << "Final size: " << sizeof(Final) << std::endl;Derived1 d1;std::cout << "d1 addr: " << &d1 << std::endl;Base *bptr = &d1;std::cout << "bptr: " << bptr << std::endl;bptr->show();Final obj;std::cout << "final obj addr: " << &obj << std::endl;Derived1 *d1_ptr = &obj;std::cout << "d1_ptr: " << d1_ptr << std::endl;d1_ptr->show();Derived2 *d2_ptr = &obj;std::cout << "d2_ptr: " << d2_ptr << std::endl;d2_ptr->show();Base *b_ptr = &obj;std::cout << "b_ptr: " << b_ptr << std::endl;b_ptr->show();return 0;
}
运行结果:
Base size: 16
Derived1 size: 32
Derived2 size: 32
Final size: 48
d1 addr: 0x7fffffffe240
bptr: 0x7fffffffe250
Derived1::show() called
final obj addr: 0x7fffffffe210
d1_ptr: 0x7fffffffe210
Final::show() called
d2_ptr: 0x7fffffffe220
Final::show() called
b_ptr: 0x7fffffffe230
Final::show() called
3. Derived1 对象的内存布局
3.1 查看 Derived1对象的内存
(gdb) p d1
$1 = {<Base> = {_vptr.Base = 0x401158 <vtable for Derived1+56>, a = 1}, _vptr.Derived1 = 0x401138 <vtable for Derived1+24>, b = 2}
注意,虽然Base部分写在了前面,但是对于虚继承,实际内存中Base被放在了后面:
(gdb) x/8x &d1
0x7fffffffe240: 0x00401138 0x00000000 0x00000002 0x00000000
0x7fffffffe250: 0x00401158 0x00000000 0x00000001 0x00000000
可以看到,对象的开头内容是 0x00401138, 也就是_vptr.Derived1 = 0x401138 <vtable for Derived1+24>, 紧接着是Derived1的int b, 然后才是_vptr.Base = 0x401158 <vtable for Derived1+56>,然后是Base的数据成员int a.
3.2 查看虚表
1) 首先查看_vptr.Derived1 = 0x401138 <vtable for Derived1+24>
(gdb) x/x 0x401138
0x401138 <vtable for Derived1+24>: 0x00400c7c
(gdb) x/i 0x00400c7c0x400c7c <Derived1::show()>: push %rbp
可见就是Derived1重写的show()
2) 继续查看 _vptr.Base = 0x401158 <vtable for Derived1+56>
(gdb) x/x 0x401158
0x401158 <vtable for Derived1+56>: 0x00400ca7
(gdb) x/i 0x00400ca70x400ca7 <virtual thunk to Derived1::show()>: mov (%rdi),%r10
(gdb) disass 0x00400ca7,+10
Dump of assembler code from 0x400ca7 to 0x400cb1:0x0000000000400ca7 <virtual thunk to Derived1::show()+0>: mov (%rdi),%r100x0000000000400caa <virtual thunk to Derived1::show()+3>: add -0x18(%r10),%rdi0x0000000000400cae <virtual thunk to Derived1::show()+7>: jmp 0x400c7c <Derived1::show()>0x0000000000400cb0 <Derived2::show()+0>: push %rbp
End of assembler dump.
可以看到,虚表中存放的是thunk代码的地址。
这段thunk代码的用途:当通过基类指针指向Derived1对象时,即Base *bptr = &d1, 由于当前的bptr指向的是Derived1对象中的Base部分(此部分位于对象靠后的位置),而非Derived1对象的真正起始地址,因此通过bptr执行虚函数时,为了执行真正的Derived1中重写的虚函数Derived1::show(),需要调整this指针到Derived1对象的起始地址。
mov (%rdi),%r10 # rdi中存放的是this指针,也就是指向Base part的指针,而非Derived对象的起始地址, 此操作将this的开头虚表地址条目也就是_vptr.Base = 0x401158 <vtable for Derived1+56> 放入r10
add -0x18(%r10),%rdi # 将_vptr.Base的虚表地址-0x18 也就是前移3个条目获取里面存放的offset, 然后将此offset 加到rdi, 执行此操作之前rdi是0x7fffffffe250, offset是0xfffffffffffffff0xf
执行add操作以后,可以得到Derived1对象的起始地址: 0x7fffffffe250 + 0xfffffffffffffff0 = 0x7fffffffe240
(gdb) x/2x 0x401158 - 0x18
0x401140 <vtable for Derived1+32>: 0xfffffff0 0xffffffff
3.3 Derived1 内存布局
其中虚表中thunk地址前面的条目0x00401200 指向typeinfo
(gdb) x/x 0x00401200
0x401200 <typeinfo for Derived1>: 0x00601d98
4. Final 对象的内存布局
4.1 查看Final对象内存
(gdb) p obj
$22 = {<Derived1> = {<Base> = {_vptr.Base = 0x401060 <vtable for Final+88>, a = 1}, _vptr.Derived1 = 0x401020 <vtable for Final+24>,b = 2}, <Derived2> = {_vptr.Derived2 = 0x401040 <vtable for Final+56>, c = 3}, d = 4}
同样,这只是逻辑视图,而不是内存实际布局。
查看实际内存布局:
(gdb) x/12x &obj
0x7fffffffe210: 0x00401020 0x00000000 0x00000002 0x00000000
0x7fffffffe220: 0x00401040 0x00000000 0x00000003 0x00000004
0x7fffffffe230: 0x00401060 0x00000000 0x00000001 0x00000000
顺序依次是Derived1 part, Derived2 part, Base part
4.2 查看虚表
1)首先查看_vptr.Derived1 = 0x401020 <vtable for Final+24>
(gdb) x/32x 0x401020
0x401020 <vtable for Final+24>: 0x00400ce4 0x00000000 0x00000010 0x00000000
0x401030 <vtable for Final+40>: 0xfffffff0 0xffffffff 0x00401188 0x00000000
0x401040 <vtable for Final+56>: 0x00400d18 0x00000000 0xffffffe0 0xffffffff
0x401050 <vtable for Final+72>: 0xffffffe0 0xffffffff 0x00401188 0x00000000
0x401060 <vtable for Final+88>: 0x00400d0f 0x00000000 0x00401020 0x00000000
0x401070 <VTT for Final+8>: 0x004010b8 0x00000000 0x004010d8 0x00000000
0x401080 <VTT for Final+24>: 0x004010f8 0x00000000 0x00401118 0x00000000
0x401090 <VTT for Final+40>: 0x00401060 0x00000000 0x00401040 0x00000000
(gdb) x/i 0x00400ce40x400ce4 <Final::show()>: push %rbp
可见虚表中的虚函数地址就是<Final::show()>
2) 继续查看_vptr.Derived2 = 0x401040 <vtable for Final+56>
(gdb) x/x 0x401040
0x401040 <vtable for Final+56>: 0x00400d18
(gdb) x/i 0x00400d180x400d18 <non-virtual thunk to Final::show()>: sub $0x10,%rdi
(gdb) disass 0x00400d18,+10
Dump of assembler code from 0x400d18 to 0x400d22:0x0000000000400d18 <non-virtual thunk to Final::show()+0>: sub $0x10,%rdi0x0000000000400d1c <non-virtual thunk to Final::show()+4>: jmp 0x400ce4 <Final::show()>End of assembler dump.
可以看到,当使用Derived2 *d2_ptr = &obj 来调用虚函数d2_ptr->show();时,需要调整this 指针,但是这个调整比较简单,就是用当前指向Derived2 part的this指针减去0x10
3) 继续查看_vptr.Base = 0x401060 <vtable for Final+88>
(gdb) x/x 0x401060
0x401060 <vtable for Final+88>: 0x00400d0f
(gdb) x/i 0x00400d0f0x400d0f <virtual thunk to Final::show()>: mov (%rdi),%r10
(gdb) disass 0x00400d0f,+10
Dump of assembler code from 0x400d0f to 0x400d19:0x0000000000400d0f <virtual thunk to Final::show()+0>: mov (%rdi),%r100x0000000000400d12 <virtual thunk to Final::show()+3>: add -0x18(%r10),%rdi0x0000000000400d16 <virtual thunk to Final::show()+7>: jmp 0x400ce4 <Final::show()>
这个就类似于上面讨论Derived1内存布局时通过基类指针操作虚函数时的this指针调整。
4.3 Final 内存布局
其中虚表中thunk地址前面的条目0x00401188指向typeinfo :
(gdb) x/i 0x00401188
0x401188 <typeinfo for Final>: cwtl