授权声明

本文为Binhua Liu原创作品。本文允许复制,修改,传递。转载请注明出处。本文发表于2010年6月16日。

原文链接:http://www.cnblogs.com/Binhua-Liu/archive/2010/06/16/1759019.html

 前言

本章节是4个课题的最后一个,我们将讨论多重继承情况下,对象内存的布局。阅读本文,请思考下面的问题:当子类从多个基类继承,虚函数指针和成员变量将如何布局?编译器如何进行子类和基类之间类型转换?如果多个基类具有同样的虚函数,子类选择哪个实现来调用?如果子类重写该虚函数,那么它覆盖的是哪个基类的实现呢?

 多重继承

我们将分析这样的例子:CFinal类继承自CBasic类和CBasic1类;CBasic类和CBasic1类都定义有虚函数add和minus;CBasic类和CBasic1类都定义有成员变量int i;子类CFinal重写了虚函数add;子类CFinal增加了新的虚函数AVG。类图如下:

代码:

class CBasic
{
  public:
      CBasic()
      {
          Array=new int[2];
      }
      int i;
      int *Array;
      virtual int add(int a,int b)
      {
          return a+b;
      }
      virtual  int minus(int a,int b)
      {
          return a-b;
      }
     void HelloWorld()
     {
         cout<<"hello world"<<endl;
     }
};
class CBasic1
{
public:
      virtual int add(int a,int b)
      {
          cout<<"CBasic1::Add"<<endl;
          return a+b;
      }
     virtual  int minus(int a,int b)
      {
          cout<<"CBasic1::Minus"<<endl;
          return a-b;
      }
     int i;
     int iBasic1;
};
class CFinal:public CBasic,public CBasic1
{        
     int add(int a,int b)
      {
          cout<<"CFinal::Add"<<endl;
          return a+b;
      }
     virtual  int AVG(int a,int b)
      {
          cout<<"CFinal::AVG"<<endl;
          return (a+b)/2;
      }
    int iFinal;
};

构造CFinal类对象:

CFinal *f=new CFinal;

我们还是用Watch窗口来观察对象的布局:

我们发现,在Watch窗口中打印f->__vfptr是不允许的,这是因为f中有2个虚函数指针,编译器不知道你想引用的是哪一个,因此我们需要把f转换为基类类型才能打印__vfptr,对于成员变量int i也是同样的。通过对内存布局的观察,我们得到这样的CFinal类内存结构图:

我们发现:

1)CBasic类对象位于CFinal类对象的前端,相应的,CBasic类的虚函数指针位于CFinal类对象的最前端。这是由于在定义CFinal类时,我们把CBasic类写在前面,编译器把它作为主基类。因此,编译器将在CBasic类的虚函数表表尾增加一个元素,来储存子类新增加的虚函数AVG的地址(请参考分析(2)中关于虚函数AVG在虚函数表中的位置的分析)。

2)CBasic1对象开始于紧接着CBasic对象结束的位置,CFinal类新增的成员变量存储在CFinal对象的尾端。

3)对于子类CFinal重写的add方法,在CBasic的虚函数表和CBasic1的虚函数表中,对应的元素都重定向为指向CFinal类的实现。对于CBasic1类来说,这种重定向是通过Thunk技术来实现的(特指VC++。本文将不讨论Thunk技术)。

下面我们讨论章节开始提出的问题,如果我们用f对象调用minus方法,哪个基类的实现会被调用?同样我们也尝试调用成员变量i,因为2个基类都定义了它。运行下面的代码:

int _tmain(int argc, _TCHAR* argv[])
{
    CFinal *f=new CFinal;
    CBasic *b=(CBasic*)f;
    f->minus(4,3);//编译错误,错误码C2385
    int x=f->i;  //编译错误,错误码C2385   
    b->minus(4,3);//成功
    int y=b->i;  //成功   return 0;
}

我们发现,直接调用minus函数或者i是不被允许的,因为编译器不知道你想调用的是哪个基类的实现!然而,通过类型转换来指定特定的基类再进行调用,则可以成功。

下一个问题,编译器如何进行子类和基类间的类型转换。对于主基类CBasic来说,这不成问题,因为它位于CFinal对象的最前端,不需要进行指针调整。那么转换为CBasic1类型呢?编译器会在CFinal对象指针的基础上加上12字节,跳过CBasic类对象从而指向CBasic1对象。你可能有这样的问题,为什么编译器知道要加上12字节,而不是13,14字节呢?这是因为编译器知道CFinal对象的布局,它清楚的知道CBasic1对象在CFinal对象中的偏移地址。如果CBasic对象的长度改变了,比如长度增加到16,需要重新编译整个程序,这样使用了CFinal对象的部分在分配地址和类型转换时,也将做出相应的改变。

关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

 结论

让我们试着为多重继承的情况做出结论(如果你对上面的内容重复一遍没有兴趣,跳过这段):

当子类从2个(或多个)带虚函数的基类继承时

1)子类中,主基类的对象内存位于子类对象内存的最前端,相应地,主基类的虚函数指针地址等于子类对象地址。

2)子类新增加的虚函数,将在主基类的虚函数表尾增加新的元素元素,来指向其实现。

3)子类中其他基类的对象位于紧接着主基类结束后的地址。

4)子类新增的成员变量位于子类对象内存的尾端。

5)在子类中重写(override)的基类虚函数,在其所有基类的虚函数表中,对应的元素都覆盖为指向子类新实现的地址(通过THUNK技术实现)。

6)对于多个基类中都定义过的虚函数,如果子类没有重写它,子类对象是不能直接调用的,因为编译器不知道你希望调用的是具体哪个基类的实现。同样,在多个子类中定义的成员变量,也不能被子类对象直接调用。

5)子类对象转换为基类类型时,除非是主基类,需要进行指针调整,指针加上若干字节,跳过位于其前端的其他基类占用的地址,从而指向需要转换的基类。编译器之所以精确的知道需要偏移的地址,因为它通过类定义清楚地知道子类对象的内存布局。

6)关于多态。对于子类中重写(override)的虚函数,在子类所有的虚函数表中对应的元素都被重定向为指向子类的新实现(如果基类有此虚函数的话),因此,无论是转换为哪一个基类,多态都能被实现。

 其他情况

至此,本文已经把所有经典常见的对象布局情况进行了研究,下面我们再简要的看几个更复杂的情形。

1,菱形多重继承。CFinal类的基类CBasic类和CBasic1类有共同的基类CInitial。

类图:

分析:这种情况和Subject4中多重继承的情况没有不同。对于编译器来说,CFinal类对2个基类的处理方式没有因为他们有共同的基类而有什么不同,只是在CFinal类对象中的CBasic类和CBasic类的内部又都分别包含了一个CInitial类。我们在Subject4种得到的所有结论在此都是适用的。

2,主基类没有虚函数的多重继承。考虑在Subject4的情况中,把CBasic类中的2个虚函数去掉,使其没有虚函数,在定义CFinal时仍把CBasic写在前面作为主虚函数。

类图:

分析:这种情况下,虽然我们把CBasic写在前面,但是CFinal类事实上把CBasic1类,即带虚函数指针和虚函数表的基类作为主基类,把它布局在对象的最前端。内存结构图:

C++内存分析(四)相关推荐

  1. CUDA统一内存分析

    CUDA统一内存分析 PascalMIG 如 NVIDIA Titan X 和 NVIDIA Tesla P100 是第一个包含页 GPUs 定额引擎的 GPUs ,它是统一内存页错误处理和 MIG ...

  2. linux下基于内存分析的rootkit检测方法

    0x00 引言 某Linux服务器发现异常现象如下图,确定被植入Rootkit,但运维人员使用常规Rootkit检测方法无效,对此情况我们还可以做什么? 图1 被植入Rootkit的Linux服务器 ...

  3. JVM虚拟机总结 内存分析及调试

    JVM工作原理和特点主要是指操作系统装入JVM是通过jdk中Java.exe来完成,通过下面4步来完成JVM环境. 1.创建JVM装载环境和配置 2.装载JVM.dll 3.初始化JVM.dll并挂界 ...

  4. Android内存分析和调优(上)

    Android内存分析和调优(上) Android内存分析和调优(上) Android内存分析工具(四):adb命令 posted on 2017-09-25 19:29 时空观察者9号 阅读(... ...

  5. Linux 系统内存分析

    1. 内存基本介绍 1.计算机基本结构: 电脑之父--冯·诺伊曼提出了计算机的五大部件:输入设备.输出设备.存储器.运算器和控制器 如图: 输入设备:键盘鼠标等 CPU:是计算机的运算核心和控制核心, ...

  6. Android内存分析工具:Memory Profiler

    一.前言  我们知道,Android系统检测到app有不再使用对象时,就会进行内存回收相关的工作. 尽管Android检测无用对象.回收内存的方法在不断改进,  但在目前所有的Android版本中,进 ...

  7. go函数详解:函数定义、形参、返回值定义规范、函数内存分析、不支持重载、支持可变参数、基本数据类型和数组默认都是值传递的、支持自定义数据类型、函数返回值命名

    引入 [1]为什么要使用函数: 提高代码的复用型,减少代码的冗余,代码的维护性也提高了 [2]函数的定义: 为完成某一功能的程序指令(语句)的集合,称为函数. [3]基本语法 func 函数名(形参列 ...

  8. C 语言中的内存分析

    C 语言中的内存分析 一.进制 我们需要了解的4中进制:二进制.八进制.十进制.十六进制 #include <stdio.h> int main() { //默认情况下是十进制 intnu ...

  9. Chrome开发者工具之JavaScript内存分析

    为什么80%的码农都做不了架构师?>>>    内存泄漏是指计算机可用内存的逐渐减少.当程序持续无法释放其使用的临时内存时就会发生.JavaScript的web应用也会经常遇到在原生 ...

最新文章

  1. ioctl()函数详解
  2. NOIP 2012 同余方程
  3. 【控制】《多智能体系统一致性协同演化控制理论与技术》纪良浩老师-第14章-带通信和输入时延的异构竞争多智能体系统分组一致性
  4. Python的集合set
  5. 十六、python沉淀之路--迭代器
  6. ASP.NET Core Blazor Webassembly 之 组件
  7. 看看你爱的他今天是什么‘颜色‘ -- Python爬取微博评论制作专属偶像词云
  8. bzoj 2752 9.20考试第三题 高速公路(road)题解
  9. [网络收集]LINUX磁盘挂载mount和共享
  10. 嗨格式视频转换器全新上线,一个音视频转换神器
  11. 液晶VGH、 VGL电路解析
  12. 这一年炼就的底层内功修养
  13. 关键词抓取规则,关键词标题SEO技巧
  14. dede标签详细的dede标签大全,dede标签在线学习
  15. 【深度学习NLP论文笔记】《Deep Text Classification Can be Fooled》
  16. Java读取环境变量
  17. 心电图学习笔记(1)
  18. 祝福你们,中国80后 (俞明洪)
  19. vue oss 上传图片
  20. 我秀秀淘宝购物导航淘宝客api程序源码版(.net源码)

热门文章

  1. android 权限录音权限检测
  2. android 解析nmea原始数据
  3. 隐形字符复制_复制器的隐形传送翘曲驱动以及科幻小说实现的可能性更高
  4. java线程间通信_java线程间通信:一个小Demo完全搞懂
  5. 【数据挖掘】 基于二手车交易价格预测-赛题分析
  6. Java 微服务中的负载均衡
  7. 联翔股份——2022年5月10日申购
  8. Windows Embedded从入门到精通6月预告
  9. python矩阵和向量乘积_NumPy 中的矩阵和向量
  10. 迈动互联中标北京人寿保险