菱形继承来自于多重继承,是为了解决具有歧义的组合而产生的一个结果。通过引入虚继承,使得多个由虚基类直接或间接派生的类拥有一个共同的基类实例。实现上相当于派生类绕过了父类直接继承了祖父类(虚基类)。
下面我们通过 MSC 14 编译器(VS2015)来看一下菱形继承的实现细节。
类继承结构如下所示:
class A0 { public: virtual ~A0() { } virtual void f() = 0; virtual void f2() = 0; int ma0; }; class A1 { public: virtual ~A1() { printf("%d", ma1); } virtual void g() = 0; virtual void g2() = 0; int ma1; }; class B : virtual public A0, virtual public A1 { public: virtual void f() { printf("B::f"); } virtual void b() { printf("B::b"); } int mb; }; class C : virtual public A0, virtual public A1 { public: virtual void f() { printf("C::f"); } virtual void c() { printf("C::c"); } int mc; }; class D : public B, public C { public: virtual void f() { printf("D::f"); } virtual void f2() { printf("D::f2"); } virtual void g() { printf("D::g"); } virtual void g2() { printf("D::g2"); } virtual void b() { printf("D::b"); } virtual void c() { printf("D::c"); } virtual void d() { printf("D::d"); } int md; };
首先使用 -d1reportAllClassLayout 编译选项查看一下所有类的内存布局:
class A0 size(8): class A1 size(8): +--- +--- 0 | {vfptr} 0 | {vfptr} // A0、A1 的虚表指针 4 | ma0 4 | ma1 // A0、A1 的成员 +--- +--- A0::$vftable@: A1::$vftable@: 0 | &A0::{dtor} 0 | &A1::{dtor} // 各自的虚表以及函数的偏移 1 | &A0::f 1 | &A1::g 2 | &A0::f2 2 | &A1::g2 class B size(28): B::$vftable@B@: // B 自身的虚表 +--- 0 | &B::b 0 | {vfptr} 4 | {vbptr} B::$vbtable@: // B 的虚基类表,由偏移 4 处 8 | mb // 的 vbptr 指向 +--- 0 | -4 // 第一项为自身(B) 相对 +--- (virtual base A0) // 于 vbptr 的偏移,即为 0 12 | {vfptr} 1 | 8 (Bd(B+4)A0) // 第一个虚基类 A0 的偏移,12 16 | ma0 2 | 16 (Bd(B+4)A1)// A1 的偏移,20 +--- +--- (virtual base A1) B::$vftable@A0@: // B 中来自于 A0 的虚函数 20 | {vfptr} 0 | &B::{dtor} 24 | ma1 1 | &B::f +--- 2 | &A0::f2 B::$vftable@A1@: // B 中来自于 A1 的虚函数 0 | &thunk: this-=8; goto B::{dtor} 1 | &A1::g 2 | &A1::g2 // C 同 B class C size(28): C::$vftable@C@: +--- 0 | &C::c 0 | {vfptr} 4 | {vbptr} C::$vbtable@: 8 | mc 0 | -4 +--- 1 | 8 (Cd(C+4)A0) +--- (virtual base A0) 2 | 16 (Cd(C+4)A1) 12 | {vfptr} 16 | ma0 C::$vftable@A0@: +--- 0 | &C::{dtor} +--- (virtual base A1) 1 | &C::f 20 | {vfptr} 2 | &A0::f2 24 | ma1 +--- C::$vftable@A1@: 0 | &thunk: this-=8; goto C::{dtor} 1 | &A1::g 2 | &A1::g2 class D size(44): D::$vftable@B@: // 来自 B 的虚函数 +--- 0 | &D::b 0 | +--- (base class B) 0 | | {vfptr} D::$vftable@C@: // 来自 C 的虚函数 4 | | {vbptr} 0 | &D::c 8 | | mb | +--- D::$vbtable@B@: // B 的虚基类表 12 | +--- (base class C) 0 | -4 12 | | {vfptr} 1 | 24 (Dd(B+4)A0) 16 | | {vbptr} 2 | 32 (Dd(B+4)A1) 20 | | mc | +--- D::$vbtable@C@: // C 的虚基类表 24 | md 0 | -4 +--- 1 | 12 (Dd(C+4)A0)// 计算得到的 A0、A1 +--- (virtual base A0) 2 | 20 (Dd(C+4)A1)// 偏移与 B 中相同 28 | {vfptr} 32 | ma0 D::$vftable@A0@: // 来自 A0 的虚函数 +--- 0 | &D::{dtor} +--- (virtual base A1) 1 | &D::f 36 | {vfptr} 2 | &D::f2 40 | ma1 +--- D::$vftable@A1@: // 来自 A1 的虚函数 0 | &thunk: this-=8; goto D::{dtor} 1 | &D::g 2 | &D::g2
对各个类的内存布局有了大致的了解后,接下来来看一下类 D 的构造和析构的情况。
·构造
01392527 push 1 ; 传入 1 01392529 mov ecx,dword ptr [ebp-0D4h] 0139252F call D::D (013911B8h) D::D: .... ; 省略函数入口代码 01391B2F pop ecx ; this 指针 01391B30 mov dword ptr [this],ecx 01391B33 mov dword ptr [ebp-0D4h],0 01391B3D cmp dword ptr [ebp+8],0 ; 为 0 跳过,防止重复构造虚基类 01391B41 je D::D+6Bh (01391B7Bh) 01391B43 mov eax,dword ptr [this] ; 设置 vbtable_B 01391B46 mov dword ptr [eax+4],offset D::`vbtable' (01397C20h) 01391B4D mov eax,dword ptr [this] ; 设置 vbtable_C 01391B50 mov dword ptr [eax+10h],offset D::`vbtable' (01397C30h) 01391B57 mov ecx,dword ptr [this] 01391B5A add ecx,1Ch ; 修正偏移后调用构造函数 01391B5D call A0::A0 (01391087h) 01391B62 or dword ptr [ebp-0D4h],1 01391B69 mov ecx,dword ptr [this] 01391B6C add ecx,24h 01391B6F call A1::A1 (0139143Dh) 01391B74 or dword ptr [ebp-0D4h],2 01391B7B push 0 ; 传入 0,防止重复构造 01391B7D mov ecx,dword ptr [this] 01391B80 call B::B (0139142Eh) 01391B85 push 0 01391B87 mov ecx,dword ptr [this] 01391B8A add ecx,0Ch 01391B8D call C::C (01391082h) 01391B92 mov eax,dword ptr [this] ; <开始 D 自身的构造> 设置 vftable_B 01391B95 mov dword ptr [eax],offset D::`vftable' (01397BE4h) 01391B9B mov eax,dword ptr [this] ; 设置 vftable_C 01391B9E mov dword ptr [eax+0Ch],offset D::`vftable' (01397BF0h) 01391BA5 mov eax,dword ptr [this] 01391BA8 mov ecx,dword ptr [eax+4] 01391BAB mov edx,dword ptr [ecx+4] 01391BAE mov eax,dword ptr [this] ; 由 B 的 vbptr 取得 A0 的虚表指针并赋值 01391BB1 mov dword ptr [eax+edx+4],offset D::`vftable' (01397BFCh) 01391BB9 mov eax,dword ptr [this] 01391BBC mov ecx,dword ptr [eax+4] 01391BBF mov edx,dword ptr [ecx+8] 01391BC2 mov eax,dword ptr [this] ; 由 B 的 vbptr 取得 A1 的虚表指针并赋值 01391BC5 mov dword ptr [eax+edx+4],offset D::`vftable' (01397C10h) 01391BCD mov eax,dword ptr [this] ; 返回 this .... ; 省略函数出口代码
可以看到,D 中保存了 4 个类各自的虚表,有了这些虚表,可以方便的将 D 类转换为对应的父类指针,而且在转为父类时,子类继承自其它父类的虚函数被屏蔽,无法调用。另外在调用 D 的构造函数中传入了一个隐含的标志参数,用来决定是否进行虚基类的构造,以此避免重复构造的问题。由此也可以知道,类中的虚表指针,以及类的虚表并不是唯一的,需要确认它的继承体系。如果 D 有非继承的虚函数,那么它将会被记录到 D 的 vftable_B 中。
·析构
D::`vector deleting destructor': .... ; 省略函数入口代码 01342010 mov dword ptr [this],ecx 01342013 mov ecx,dword ptr [this] 01342016 sub ecx,1Ch ; 将 this 从 A0 调整为 D 01342019 call D::`vbase destructor' (013410D7h) 0134201E mov eax,dword ptr [ebp+8] 01342021 and eax,1 01342024 je D::`scalar deleting destructor'+47h (01342037h) 01342026 push 2Ch 01342028 mov eax,dword ptr [this] 0134202B sub eax,1Ch 0134202E push eax 0134202F call operator delete (01341055h) 01342034 add esp,8 01342037 mov eax,dword ptr [this] .... ; 省略函数出口代码 D::`vbase destructor': .... ; 省略函数入口代码 01341DA0 mov dword ptr [this],ecx 01341DA3 mov ecx,dword ptr [this] 01341DA6 add ecx,1Ch ; this 改为 A0 01341DA9 call D::~D (0134146Ah) 01341DAE mov ecx,dword ptr [this] 01341DB1 add ecx,24h 01341DB4 call A1::~A1 (013410A0h) 01341DB9 mov ecx,dword ptr [this] 01341DBC add ecx,1Ch 01341DBF call A0::~A0 (01341456h) .... ; 省略函数出口代码 D::~D: .... ; 省略函数入口代码 01341D40 mov dword ptr [this],ecx 01341D43 mov ecx,dword ptr [this] 01341D46 sub ecx,4 01341D49 call C::~C (0134132Ah) 01341D4E mov ecx,dword ptr [this] 01341D51 sub ecx,10h 01341D54 call B::~B (013411D6h) .... ; 省略函数出口代码
析构过程中直接将虚基类的调用放在了最后,依次执行父类的析构之后,再调用虚基类的析构。而不是将其包含在父类中,再像构造的时候用标志位区分是否构造。自然,B、C 的析构函数就不再含有对 AX::~AX 的调用了。
有趣的是,当给 C 加上析构函数时,类的布局会发生变化。编译器会分别在原先的基础上,在 A0、A1 前加上 4 字节的 vtordisp 填充,变为
1> class D size(52): 1> +--- 1> 0 | +--- (base class B) 1> 0 | | {vfptr} 1> 4 | | {vbptr} 1> 8 | | mb 1> | +--- 1> 12 | +--- (base class C) 1> 12 | | {vfptr} 1> 16 | | {vbptr} 1> 20 | | mc 1> | +--- 1> 24 | md 1> +--- 1> 28 | (vtordisp for vbase A0) <--- 1> +--- (virtual base A0) 1> 32 | {vfptr} 1> 36 | ma0 1> +--- 1> 40 | (vtordisp for vbase A1) <--- 1> +--- (virtual base A1) 1> 44 | {vfptr} 1> 48 | ma1 1> +---
查看了一下似乎是在析构函数中,具体来说是在 D::~D 中(此时的 this 为 A0)调用 C::~C 前将 ecx 设置为 vtordisp for vbase A0,随后将其视为 this 进行 vptr 以及成员的定位。
微软的解释是重写了虚函数并在构造/析构函数里调用了重写的函数时会插入这个字段,定义了 D(或 B、C)的析构函数满足了这个要求,而编译器自动生成的析构函数似乎不算在内。