菱形结构的虚继承

  这次我们看看菱形结构的虚继承。虚继承的引入本就是为了解决复杂结构的继承体系问题。上一篇我们在讨论虚继承时用的是一个简单的继承结构,只是为了打个铺垫。
  我们先看看这几个类,这是一个典型的菱形继承结构。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],编译器在生成代码时会直接把最后一步运算做掉。

  (未完待续)

潘凯:C++对象布局及多态实现的探索(九)相关推荐

  1. 潘凯:C++对象布局及多态实现的探索(十)

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

  2. 潘凯:C++对象布局及多态实现的探索(六)

    虚函数调用 我们再看看虚成员函数的调用.类C041中含有虚成员函数,它的定义如下: struct C041 {     C041() : c_(0x01) {}     virtual void fo ...

  3. 潘凯:C++对象布局及多态实现的探索(一)

    前言 本文通过观察对象的内存布局,跟踪函数调用的汇编代码.分析了C++对象内存的布局情况,虚函数的执行方式,以及虚继承,等等. 写这篇文章源于我在论坛上看到的一个贴子.有人问VC使用了哪种方式来实现虚 ...

  4. 潘凯:C++对象布局及多态实现的探索(三)

    带虚函数的类的对象布局(2) 接下来我们看看多重继承.定义两个类,各含一个虚函数,及一个数据成员.再从这两个类派生一个空子类. struct C041 {     C041() : c_(0x01) ...

  5. 潘凯:C++对象布局及多态实现的探索(十一)

    菱形结构的虚继承(3) 最后我们看看,如果在上篇例子的基础上,子类及左.右父类都各自定义了自己的虚函数,这时的情况又会怎样. struct C140 : public virtual C041 {   ...

  6. 潘凯:C++对象布局及多态实现的探索(八)

    普通的虚继承 下面我们来看虚继承.首先看看这C020类,它从C010虚继承: struct C010 {     C010() : c_(0x01) {}     void foo() { c_ = ...

  7. 潘凯:C++对象布局及多态实现的探索(二)

    带虚函数的类的对象布局(1) 如果类中存在虚函数时,情况会怎样呢?我们知道当一个类中有虚函数时,编译器会为该类产生一个虚函数表,并在它的每一个对象中插入一个指向该虚函数表的指针,通常这个指针是插在对象 ...

  8. 潘凯:C++对象布局及多态实现的探索(七)

    构造函数中的虚成员函数调用 在构造函数中调用虚成员函数,虽然这是个不很常用的技术,但研究一下可以加深对虚函数机制及对象构造过程的理解.这个问题也和一般直观上的认识有所差异.先看看下面的两个类定义. s ...

  9. 潘凯:C++对象布局及多态实现的探索(十二)

    后记 结合前面的讨论,我们可以看到,只要牵涉到了虚继承,在访问父类的成员变量时生成的代码相当的低效,需要通过很多间接的计算来定位成员变量的地址.在指针类型转换,动态转型,及虚函数调用时,也需要生成很多 ...

最新文章

  1. oracle 当前年到指定年的年度范围求取
  2. 查看springboot中tomcat版本
  3. 拦截器 java_在Java后端如何添加拦截器
  4. Linux日志系统分析:rsyslog、syslog和klog
  5. 【C语言进阶深度学习记录】二十八 数组指针与指针数组的分析
  6. JavaScript + CSS3 实现的海报画廊特效
  7. JS中return SomeFunction和不加return的区别
  8. linux su文件删除了,UBUNTU linux 批量删除文件
  9. 电子白板计算机培训心得,电子白板培训个人心得体会范文
  10. 人机智能的逻辑哲学论
  11. Oracle卸载:指定数据库的诊断目标位置不存在解决
  12. runtime无法执行grep_Runtime.getRuntime.exec()执行linux脚本导致程序卡死有关问题
  13. “火星一号”项目计划于2026年实现载人登陆火星
  14. C Programming学习笔记【谭浩强老师编】(第四章选择结构程序设计)02 逻辑运算符和逻辑表达式
  15. 【自动化毕业设计】基于机械视觉控制的板球控制装置
  16. 豆豆趣事[2013年09月]
  17. 关于在美国的追债公司~
  18. 九天毕昇”云平台:python3.7+CUDA10.1+torch1.6.0+spconcv1.2.1安装OpenPCDet全流程
  19. Python基础课程-for循环
  20. kuka机器人外部轴配置方法

热门文章

  1. 综述 | 注意力机制「AI核心算法」
  2. 【工业互联网】海尔集团数字科技赵光:工业互联网垂直行业的落地实践
  3. 【Centos 7】解决win10家庭版共享文件夹失败,导致linux系统虚拟机无法完成挂载任务的问题
  4. cadence快捷键总结(转)
  5. 精益数据分析 - 第13章 商业模式六:双边市场
  6. 爬虫学习记录3:request库基础用法
  7. AVPlayer播放视频(本地视频,或网络视频)
  8. 天涯明月刀自动演奏器,弹琴脚本,易语言开发,实现键盘映射
  9. 《HyperLedger Fabric 实战》—— 十、项目演练 -- 反欺诈系统
  10. 卖家亲身经历,海外网红营销如何引流独立站