普通的虚继承

  下面我们来看虚继承。首先看看这C020类,它从C010虚继承:
struct C010
{
    C010() : c_(0x01) {}
    void foo() { c_ = 0x02; }
    char c_;
};
struct C020 : public virtual C010
{
    C020() : c_(0x02) {}
    char c_;
};
  运行如下代码,查看对象的内存布局:
PRINT_SIZE_DETAIL(C020)
  结果为:
The size of C020 is 6
The detail of C020 is c0 c2 45 00 02 01
  很明显对象的起始处是一个指针,然后是子类的成员变量,接下来是父类的成员变量。和以前的讨论不同的是由于使用了虚继承,父类的成员变量被放到了最后面。
  运行如下的代码:
C020 c020;
c020.C010::c_ = 0x04;
  由于子类中的变量和父类中的变量重名,所以我们必须用这种方式来访问属于父类的成员变量,普通情况下不需要这种写法。我们看看后面这行代码对应的汇编代码:
0042387E  mov         eax,dword ptr [ebp+FFFFF82Ch]
00423884  mov         ecx,dword ptr [eax+4]
00423887  mov         byte ptr [ebp+ecx+FFFFF82Ch],4
  前面说过对象的起始是一个指针,第1行指令取到这个指针的值,第2行把这个指针指向的地址后移4字节后的值(做为一个4字节的值)取出来。执行完这句我们看看ecx寄存器,可知取出来的值为5。最后一行是真正的赋值指令,它通过在对象的起始处(即[ebp+FFFFF32Ch])加上ecx中的值做偏移值(即5)来得到赋值的目的地址。接合前面的对象布局输出,我们可以发现从对象起始地址开始加5字节的偏移值,刚好得到父类的成员变量的地址。这样我们可以大致分析出直接虚继承的子类的对象布局。
|子类5             |父类1    |
|偏移值指针4,5|子类成员变量1|父类成员变量1|
  (注:第一个数字为所在区域的长度(字节数),偏移值指针后的第二个数字为该指针指向的偏移值。后同。)
  通过查看内存可以发现偏移值指针指向的内存前4字节为0,我不知道它的具体的用途是什么。接下来的4字节是一个32位的整数,也就是真正的偏移值。即从子类的起始位置到被虚继承的父类的起始位置的偏移值,在我们前面的例子中这个值为5(一个指针加一个char成员变量)。
  通过这个分析我们可以看到在虚承继的情况下,通过子类的对象访问父类的普通成员变量的效率是相当低的。如果必须用到虚继承,也应该尽量不要在父类中放置普通成员变量(静态成员变量不受影响)。
  另外为什么微软不把偏移值直接放到子类中,而是采用偏移值指针。我想是因为采用指针的方式更为灵活,即使以后需要扩展也不影响类对象的布局。

  按下来我们再看看这几行代码:
PRINT_OBJ_ADR(c020);
C010 * pt = &c020;
PRINT_PT(pt);
pt->c_ = 0x03;
  第2行声明了一个父类指针,并让它指向一个子类的对象。第3行打印出这个指针的值。运行结果为:
c020's address is : 0012F708
pt's value is : 0012F70D
  我们可以看到赋值后的指针的值并不等于赋给它的对象地址值。也就是说在这个赋值过程中编译器进行了额外的工作,即调整了指针的值。我们看看第2行对应的汇编代码,看看编译器究竟做了些什么?
01 004238EA  lea         eax,[ebp+FFFFF82Ch]
02 004238F0  test        eax,eax
03 004238F2  jne         00423900
04 004238F4  mov         dword ptr [ebp+FFFFF014h],0
05 004238FE  jmp         00423916
06 00423900  mov         ecx,dword ptr [ebp+FFFFF82Ch]
07 00423906  mov         edx,dword ptr [ecx+4]
08 00423909  lea         eax,[ebp+edx+FFFFF82Ch]
09 00423910  mov         dword ptr [ebp+FFFFF014h],eax
10 00423916  mov         ecx,dword ptr [ebp+FFFFF014h]
11 0042391C  mov         dword ptr [ebp+FFFFF820h],ecx
  喔!比想象的要复杂的多。一行简单的指针赋值语句却产生了这么多的汇编代码。这行代码本身的语义是取对象的地址赋给一个指针,对于编译器来说它把这做为指针到指针的赋值来处理。由于牵涉到了向上的类型转换,同时又有虚继承存在。根据前面的布局分析,在虚继承的情况下,父类位于对象布局的后部。因此在这里要做一个指针位置的调整。由于调整要根据源指针来进行计算,所以先要对源指针的合法性进行检查,以避免运行时的指针异常错误。前3行的汇编指令就是在做这件事,检查源指针是否为NULL。如果为NULL则执行4、5、10、11行,最终给pt赋0。如果不为NULL跳至第6行执行到最后。重要的是第6、7、8行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的父类部分的数据成员。
  对比一下普通的指针赋值,我们可以对上面赋值的复杂性和低效有更深的认识。
C010 * pt1 = NULL;
C010 * pt2 = pt1;
  这两行相应的汇编代码为:
0042397D  mov         dword ptr [ebp+FFFFF814h],0
00423987  mov         eax,dword ptr [ebp+FFFFF814h]
0042398D  mov         dword ptr [ebp+FFFFF808h],eax
  第1行是普通的赋值,编译器并不做任何的检查,即使源指针为NULL。因为它不需要根据源指针(本处为NULL)做任何计算。第2个赋值也很直接,只是通过eax做了一个中转。这里我们就可以看到前面的虚继承下的子类指针到父类指针的赋值是我么的低效。在程序中应尽量的避免这种代码。

  (未完待继)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最新文章

  1. mysql+esc,mysql(3):基础,常用命令句使用(2)--上集
  2. 逻辑心理测试题:三囚分汤
  3. java中创建对象的方式
  4. c++实现读写共享锁
  5. 查看用户过期linux,linux下控制帐户过期的多种方法讲解
  6. 网络命令大全(9)--runas
  7. 方正高影仪安装方法_铝合金门窗是怎么安装的?
  8. Pandas最详细教程来了!
  9. android 返回键退出程序了吗?
  10. Spring的AOP面向切面编程
  11. 利用sklearn进行豆瓣电影评论的文本分类
  12. 互联网对实体经济的三轮冲击
  13. 我国国防是全军的国防_国防部长
  14. 游戏角色写实头发制作
  15. pytorch学习 -- 反向传播backward
  16. 吴军《罗曼·罗兰 | 年轻时为什么需要理想主义?》
  17. 苹果面临集体诉讼 因涉嫌销售iTunes和Apple Music用户数据
  18. photoshop扣发丝——就这么简单
  19. 系统监理工程师学习笔记
  20. Flutter2.0重磅发布!带你一文打尽Flutter Engage

热门文章

  1. Glide SimpleTarget 过时
  2. 实现线程同步的几种方式
  3. help大佬看看我!!!!!!!!!!!!!!
  4. How to improve your ability of presentation,让报告变得跟轻松!
  5. 吴恩达deeplearning.ai系列课程笔记+编程作业(13)序列模型(Sequence Models)-第一周 循环序列模型(Recurrent Neural Networks)
  6. RFIC4463_F2B
  7. 电子工程师,你真的了解Type-C吗?
  8. 构建中国云生态 | 华云数据与华为完成产品兼容互认证 被授予华为V认证
  9. matplotlib数据可视化
  10. 爬取国家统计数据_GDP