这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。上一篇我们在讨论虚继承时用的是一个简单的继承结构,只是为了打个铺垫。
我们先看看这几个类,这是一个典型的菱形继承结构。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
我们可以象上一篇一样,画出对象的内存布局。
|c100,5 |c101,5 |c110,1 |c041,5 |
|ospt,4,11 |m,1 |ospt,4,6 |m,1 |m,1 |vtpt,4 |m1 |
(注:为了不折行,我用了缩写。ospt代表偏移值指针、m代表成员变量、vtpt代表虚表指针。第一个数字是该区域的大小,即字节数。只有偏移值指针有第二个数字,第二个数字就是偏移值指针指向的偏移值的大小。)
可以看到对象的内存布局中只有一个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