【IT168技术文档】
这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。上一篇我们在讨论虚继承时用的是一个简单的继承结构,只是为了打个铺垫。
我们先看看这几个类,这是一个典型的菱形继承结构。C100和C101通过虚继承共享同一个父类C041。C110则从C100和C101多重继承而来。
运行如下代码:struct C041 { C041() : c_(0x01) {} virtual void foo() { c_ = 0x02; } char c_; }; struct C100 : public virtual C041 { C100() : c_(0x02) {} char c_; }; struct C101 : public virtual C041 { C101() : c_(0x03) {} char c_; }; struct C110 : public C100, public C101 { C110() : c_(0x04) {} char c_; };
结果为:PRINT_SIZE_DETAIL(C110)
我们可以象上一篇一样,画出对象的内存布局。The size of C110 is 16 The detail of C110 is 28 c3 45 00 02 1c c3 45 00 03 04 18 c3 45 00 01
(注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的大小,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的大小。)|C100,5 |C101,5 |C110,1 |C041,5 | |ospt,4,11 |m,1 |ospt,4,6 |m,1 |m,1 |vtpt,4 |m1 |
可以看到对象的内存布局中只有一个C041,即祖父类的部分只有一份,且放在最后面。这就是菱形继承。对比前面几篇的讨论,我们可以知道,如果没有用虚继承机制,那么在C041对象的内存布局中会出现两份C041部分,这也就是所谓的V型继承。相应的对象布局为:C041+C100+C041+C101 +C110。在V型继承中是不能直接从C110,即孙子类,直接转型到C041,即祖父类的。因为在对象的布局中有两份祖父类的实体,一份从C100而来,一份从C101而来。编译器在决议时会存在二义性,它不知道转型后到底用哪一份实体。虽然可以通过先转型到某一父类,然后再转型到祖父类来解决。但使用这种方法时,如果改写了祖父类的成员变量的内容,runtime是不会同步两个祖父类实体的状态,因此可能会有语义错误。
我们再分析一下上面的内存布局。普通继承的布局,顶层类在前面。多重继承时则按从左到右的顺序排。从C100和C101到C110的继承是普通继承,所以遵循这个原则,先是左父类再右父类,接下去是子类。而虚继承则要求将共享的父类放到整个对象布局的最后(即使虚父类没有被真正的共享也是如此,前在一篇的C020 类就是这样。不知道打开优化开关后会不会有变化。)所以在上例中的祖父类也是被置于最后的。
我们再看看对成员的访问情况。运行以下代码并查看相应的汇编代码。
对应的汇编代码为:C110 c110; c110.c_ = 0x51; c110.C100::c_ = 0x52; c110.C101::c_ = 0x52; c110.C041::c_ = 0x53; c110.foo();
01 00423993 push 1 02 00423995 lea ecx,[ebp+FFFFF7F0h] 03 0042399B call 0041DE60 04 004239A0 mov byte ptr [ebp+FFFFF7FAh],51h 05 004239A7 mov byte ptr [ebp+FFFFF7F4h],52h 06 004239AE mov byte ptr [ebp+FFFFF7F9h],52h 07 004239B5 mov eax,dword ptr [ebp+FFFFF7F0h] 08 004239BB mov ecx,dword ptr [eax+4] 09 004239BE mov byte ptr [ebp+ecx+FFFFF7F4h],53h 10 004239C6 mov eax,dword ptr [ebp+FFFFF7F0h] 11 004239CC mov ecx,dword ptr [eax+4] 12 004239CF lea ecx,[ebp+ecx+FFFFF7F0h] 13 004239D6 call 0041DF32