C 对象布局及多态探索之菱形结构虚继承

2008-02-23 05:27:16来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

  这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。上一篇我们在讨论虚继承时用的是个简单的继承结构,只是为了打个铺垫。

  我们先看看这几个类,这是个典型的菱形继承结构。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

  前3行是对象的初始化,调用了对象的构造函数。4、5、6行是对子类、左右父类的成员变量的赋值。我们能够看到是直接写的,因为这一层的继承是普通继承。第7、8、9行是对祖父类成员变量的赋值,和上篇讨论过的相同,是通过偏移值指针指向的偏移值来间接访问的。最后的4行指令是对成员函数的调用。我们能够看到调用的函数地址是直接给出的(最后一行),因为我们是通过对象来调用,即使是虚函数调用也不会有多态的行为。但是得到this指针的方式却是颇为间接,即第10、11、12行。因为这个函数在祖父类中定义,那么他操作的数据成员应该是祖父类的。因此编译器要调整this指针的位置。而祖父类又是被虚继承,因此要通过偏移值指针指向的偏移值来进行调整。

  再观察一下第9行和第12行,能够看到计算出来的地址值是不相同的。这是因为第9行为给祖父类的成员变量赋值,而祖父类中有虚表指针存在,所以在得到对象的起始地址后,编译器给他加了4字节的偏移量以跳过虚指针。实际的得到地址的运算为: [ebp ecx FFFFF7F0h 4h],编译器在生成代码时会直接把最后一步运算做掉。

  我们再看一个例子,这个例子的继承结构和上一篇中是相同的,也是菱形结构。不同的是,每一个类都重写了顶层类声明的虚函数。代码如下:

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C140 : public virtual C041
{
 C140() : c_(0x02) {}
 virtual void foo() { c_ = 0x11; }
 char c_;
};

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: C 箴言:如何访问模板化基类中的名字

下一篇: C 箴言:为类型信息使用特征类