本文有更新,请移步我的个人博客:https://blog.andyqiao.top/article/18/

主要从三个方面来讲:

  1 单一继承

  2 多重继承

  3 虚拟继承

1 单一继承

(1)派生类完全拥有基类的内存布局,并保证其完整性。

  派生类可以看作是完整的基类的Object再加上派生类自己的Object。如果基类中没有虚成员函数,那么派生类与具有相同功能的非派生类将不带来任何性能上的差异。另外,一定要保证基类的完整性。实际内存布局由编译器自己决定,VS里,把虚指针放在最前边,接着是基类的Object,最后是派生类自己的object。举个栗子:

class A
{
int b;
char c;
};
class A1 :public A
{
char a;
};
int main()
{
cout << sizeof(A) << " " << sizeof(A1) << endl;
return ;
}

  输出是什么?

  答案:

    8 12

  A类的话,一个int,一个char,5B,内存对齐一下,8B。A1的话,一个int,两个char,内存对齐一下,也是8B。不对吗?

  我说了,要保证基类对象的完整性。那么一定要保证A1类前面的几个字节一定要与A类完全一样。也就是说,A类作为内存补齐的3个字节也是要出现在A1里面的。也就是说,A类是这样的:int(4B)+char(1B)+padding(3B)=8B,A1类:int(4B)+char(1B)+padding(3B)+char(1B)+padding(3B)=12B。

(2)虚指针怎么处理?

  还是视编译器而定,VS是永远把vptr放在对象的最前边。如果基类中含有虚函数,那么处理情况与上边一样。可是,如果基类中没有虚函数而派生类有的话,那么如果把vptr放在派生类的前边的话,将会导致派生类中基类成分并不在最前边。这将带来什么问题呢?举栗:假设A不含虚,而A1含。

A *pA;
A1 obj_A1;
pA=&obj_A1;

  如果A1完全包含A并且A位于A1的最前边,那么编译器只需要把&obj_A1直接赋给pA就可以了。如果不是呢?编译器就需要把&obj_A1+sizeof(vptr)赋给pA了。

2 多重继承

  说结论:VS的内存布局是按照声明顺序排列内存。再举个栗子:

class point2d
{
public:
virtual ~point2d(){};
float x;
float y;
};
class point3d :public point2d
{
~point3d(){};
float z;
};
class vertex
{
public:
virtual ~vertex(){};
vertex* next;
};
class vertex3d :public point3d, public vertex
{
float bulabula;
}; int _tmain(int argc, _TCHAR* argv[])
{
cout << sizeof(point2d) << " " << sizeof(point3d) << " " << sizeof(vertex) << " " << sizeof(vertex3d) << endl;
return ;
}

  输出: 12 16 8 24。

  内存布局:

    point2d: vptr(4)+x(4)+y(4)=12B

    point3d: vptr+x+y+z=16B

    vertex: vptr+next=8B

    vertex3d: vptr+x+y+z+vptr+next+bulabula=28B

为什么需要多个虚指针?请往下看。

3 虚拟继承

(1)为什么要有“虚继承”这样的机制?

  简单讲,虚继承是为也防止“diamond”继承所带来的问题。也就是类A1、A2都继承于A,类B又同时继承于A1、A2。这样一来,类B中就有两份类A的成员了,这样的程序无法通过编译。我们改成这样的形式:

class A
{
public:
  int a;
  virtual ~A();
virtual void fun(){cout<<"A"<<endl;}
};
class A1 :public virtual A
{
public:
  int a1;
virtual void fun(){cout<<"A1"<<endl;}
};
class A2 :public virtual A
{
public:
  int a2;
virtual void fun(){cout<<"A2"<<endl;}
}; class B :public A1,public A2 {
public:
  int b;
virtual void fun(){cout<<"B"<<endl;}
virtual void funB(){};
};

  这样就能防止这样的事情发生。

(2)虚拟继承与普通继承的区别:

  普通继承使得派生类每继承一个基类便拥有一份基类的成员。而虚拟继承会把通过虚拟继承的那一部分,放在对象的最后。从而使得只拥有一份基类中的成员。虚拟对象的偏移量被保存在Derived类的vtbl的this指向的上一个slot。比较难理解。下面我给你个栗子。

(3)虚拟继承的内存布局:

  每个派生类会把其不变部分放在前面,共享部分放在后面。

上面四个类的大小是怎样的呢?

int _tmain(int argc, _TCHAR* argv[])
{
cout << sizeof(A) << " " << sizeof(A1) << " " << sizeof(A2) << " " << sizeof(B) << endl;
return ;
}

  输出:8 16 16 28

  内存布局:

    A: vptr+a=8B

    A1: vptr+a1+vptrA+a=16B

    A2: vptr+a2+vptrA+a=16B

    A3: vptr+a1+vptrA2+a2+b+vptrA+a=28B

  上个草图:

  那究竟为什么需要多个虚指针?将对象内存布局和虚表结构搞清楚之后,答案是不是呼之欲出呢?

是的,因为这样可以保证在将子类指针/引用转换成基类指针时编译器可以直接根据对像的内存布局进行偏移,从而使得指向的第一个内容为虚指针,进而实现多态(根据静态类型执行相应动作)。

C++中派生类对象的内存布局的更多相关文章

  1. c++类对象的内存分布

    要想知道c++类对象的内存布局, 可以有多种方式,比如: 1)输出成员变量的偏移, 通过offsetof宏来得到 2)通过调试器查看, 比如常用的VS 1.没有数据成员的对象 class A{ }; ...

  2. HotSpot源码分析之C++对象的内存布局

    HotSpot采用了OOP-Klass模型来描述Java类和对象.OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象的具体类型.为了更好理解这个模型, ...

  3. VS中C++对象的内存布局

    本文主要简述一下在Visual Studio中C++对象的内存布局,这里没有什么测试代码,只是以图文的形式来描述一下内存分布,关于测试的代码以及C++对象模型的其他内容大家可以参考一下陈皓先生的几篇博 ...

  4. JVM中对象的内存布局与访问定位

      一.对象的内存布局 已主流的HotSpot虚拟机来说,   在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header).实例数据(Instance Data)和对齐填 ...

  5. 基类中定义的虚函数在派生类中重新定义时,其函数原型,包括返回类型、函数名、参数个数、参数类型及参数的先后顺序,都必须与基类中的原型完全相同 but------> 可以返回派生类对象的引用或指针

      您查询的关键词是:c++primer习题15.25 以下是该网页在北京时间 2016年07月15日 02:57:08 的快照: 如果打开速度慢,可以尝试快速版:如果想更新或删除快照,可以投诉快照. ...

  6. C++中的类所占内存空间总结

    C++中的类所占内存空间总结    最近在复习c++的一些基础,感觉这篇文章很不错,转载来,大家看看! 类所占内存的大小是由成员变量(静态变量除外)决定的,成员函数(这是笼统的说,后面会细说)是不计算 ...

  7. C++ 对象的内存布局(上)

    C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击这里查看下篇>>> 前言 07年12月,我写了一篇<C++虚函数表解析>的文 ...

  8. JVM——深入分析对象的内存布局

    概述 一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的方法存储在方法区中的.Class本身就是一个对象,都以KB为单位,如果new Integer()为了表示一个数据就占用KB级别的内 ...

  9. Java对象的内存布局

    对象的内存布局 平时用java编写程序,你了解java对象的内存布局么? 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域: 对象头 实例数据 对齐填充 对象头 对象头包括两部分信息: ...

随机推荐

  1. LoRaWAN协议(二)--LoRaWAN MAC数据包格式

    名词解析 上行:终端的数据发送经过一个或多个网关中转到达网络服务器. 下行:由网络服务器发送给终端设备,每条消息对应的终端设备是唯一确定的,而且只通过一个网关中转. LoRaWAN Classes L ...

  2. 【转载】dirs、pushd、popd指令

    转载自:http://hi.baidu.com/linuxtrip/item/47a38c8eec00e98f4414cfb7 一.目录栈指令 目录栈是用户最近访问过的系统目录列表,并以堆栈的形式管理 ...

  3. Python单元测试框架之pytest -- fixtures

    fixtures不太好翻译,可看作是夹心饼干最外层的两片饼干.通常用setup/teardown来表示.它主要用来包裹测试用例,为什么需要这样的饼干呢?我们以web自动化测试为例,例如,要测试的某系统 ...

  4. Android 学习笔记之Volley开源框架解析(五)

    学习内容: 1.PoolingByteArrayOutputStream 2.ByteArrayPool 3.HttpStack 4.HurlStack 5.HttpHeaderParser   前面 ...

  5. Java魔法堂:以Windows服务的形式运行Java程序

    一.前言 由于防止维护人员误操作关闭Java控制台程序,因此决定将其改造为以Windows服务的形式运行.弄了一个上午总算搞定了,下面记录下来,以供日后查阅. 二.Java Service Wrapp ...

  6. [团队项目]expat不兼容BUG

    本周五软工团队项目的第一次前后端全部对接时,出了一个蛋疼的错误. 最初起因是小丽叔出于安全的考虑,使用守护进程来跑Web服务器.守护进程(Daemon)是运行在后台的一种特殊进程,如果服务器用root ...

  7. tomcat端口被占用

    这里就以win7为例进行讲解. 首先打开cmd,打开的方法很简单,在开始菜单中直接输入即可.同样也可以按下win+R打开运行,然后输入cmd即可. 在dos命令中输入以下命令查询正在被使用的端口号以及 ...

  8. [CLR via C#]14. 字符、字符串和文本处理

    一.字符 在.NET Framewole中,字符总是表示成16位Unicode代码值,这简化了国际化应用程序的开发. 每个字符都表示成System.Char结构(一个值类型) 的一个实例.System ...

  9. Python入门笔记(17):错误、异常

    一.什么是错误,什么是异常,它们两者区别 这里解释如下:个人觉得很通俗易懂 错误是指在执行代码过程中发生的事件,它中断或干扰代码的正常流程并创建异常对象.当错误中断流程时,该程序将尝试寻找异常处理程序 ...

  10. 【jQuery基础学习】12 jQuery学习感想

    学习完<锋利的jQuery>,用时13天. 这期间,私底下又用了一点时间去W3C上把HTML和CSS重新过了一遍. 总的来说,收获还是蛮多的. 其实在本书里面真正重要的也就前几章,后面的都 ...