C 对象布局及多态探索之菱形结构虚继承
2008-02-23 05:27:16来源:互联网 阅读 ()
我们先看看这几个类,这是个典型的菱形继承结构。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 箴言:为类型信息使用特征类
相关文章
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设 网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源 网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它 服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护 软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷 网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash 热门词条
最新资讯
热门关注
热门标签
|