C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章

在上面这篇文章的基础上做了些整理。

主要讨论了C++类对象的内存分布结构。

来看看编译器是怎么处理类成员内存分布的,特别是在继承虚函数存在的情况下。

原文地址:http://www.cnblogs.com/jerry19880126/p/3616999.html


0、准备

工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来:

选择左侧的C/C++->命令行,在其他选项这里写上

  • /d1 reportAllClassLayout 它可以看到所有相关类的内存布局,
  • 如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。

1、普通类

下面可以定义一个类,像下面这样:

class Base{
int a;
int b;
public:
void CommonFunction();
};

编译后,可以看到输出框里面有这样的排布:

这里不想花精力在内存对齐因素上,所以成员变量都设为int型。

从这里可以看到普通类的排布方式:

  • 成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

2、普通继承

再看下继承,往后面添加如下代码:

class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
};

编译,然后看到如下的内存分布(父类的内存分布不变,这里只讨论子类成员变量的内存分布):

可以看到子类继承了父类的成员变量

在内存排布上:

-先是排布了 父类的成员变量 ,接着排布 子类的成员变量,同样,成员函数不占字节。


3、包含虚函数的类

下面给基类加上虚函数,暂时注释掉DerivedClass,看一下这时的内存排布:

class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
};



这个内存结构图分成了两个部分:上面是内存分布,下面是虚表

  1. 内存分布

    VS带的编译器把虚函数表指针放在了内存的开始处(地址偏移为0),然后再是成员变量;
  2. 虚函数表

    紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布;

    下面列出了虚函数,左侧的0是这个虚函数 的序号,这里只有一个虚函数,所以只有一项;

    如果有多个虚函数,会有序号为1、2...的虚函数列出来。
  • 编译器是在调用构造函数时创建这个虚表指针以及虚表的。

4、包含虚函数的继承

下面加上子类,并在子类中添加虚函数,像下面这样:

class DerivedClass: public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};

可以看到子类内存的排布如下:

  • 上半部是内存分布,可以看到,

    虚表指针被继承了,且仍位于内存排布的起始处,

    下面是父类的成员变量a和b,最后是子类的成员变量c,

    注意虚表指针只有一个,子类并没有再生成虚表指针了;
  • 下半部的虚表情况与父类(3、包含虚函数的类)是一样的。

5、多态实现

编译器是如何利用虚表指针与虚表来实现多态的呢?

  • 当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数表;
  • 当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

所以,如果是调用

Base *p = new Derived();
  1. 生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,
  2. 接着由Derived*Base*隐式类型转换并没有改变虚表指针
  3. 若调用p->VirtualFunction(),实际上是p->vfptr->VirtualFunction()

    它在构造的时候就已经指向了子类的VirtualFunction(),所以调用的是子类的虚函数。

这就多态了。


6.1、变化1

我们把子类换个代码,像这样:

class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction2();
};

注意到这时我们并没有覆盖父类的虚方法,而是重声明了一个新的子类虚方法,内存分布如下:

还是只有一个虚表指针,但是下方虚表的内容变化了,

  • 虚表的0号是父类的VirtualFunction(),而1号放的是子类的 VirtualFunction2()

也就是说,如果定义了DerivedClass的对象(或者指向它的指针或引用),那么在构造时,虚表指针就会指向这个虚表;

如果调用的是VirtualFunction(),那么会从父类中寻找对应的虚函数(!子类没有覆盖父类的函数);

如果调用的是VirtualFunction2(),那么会从子类中寻找对应的虚函数。

????子类没有重载父类的虚函数是一件毫无意义的事情


6.2、变化2

我们再改造一下子类,像这样:

class DerivedClass1 : public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
virtual void VirtualFunction2();
};

我们既覆盖父类的虚函数,也有新添的虚函数,那么可以料想的到,是下面的这种内存分布:


7、多重继承

下面来讨论多重继承,代码如下:

class Base{
int a;
int b;
public:
void CommonFunction();
virtual void VirtualFunction();
}; class DerivedClass1: public Base{
int c;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
}; class DerivedClass2 : public Base{
int d;
public:
void DerivedCommonFunction();
virtual void VirtualFunction();
}; class DerivedDerivedClass : public DerivedClass1, public DerivedClass2{
int e;
public:
void DerivedDerivedCommonFunction();
virtual void VirtualFunction();
};

内存分布从父类到子类,依次如下:



  1. Base中有一个虚表指针,地址偏移为0


  2. DerivedClass1继承了Base,内存排布是先父类后子类。


  3. DerivedClass2的情况是类似于DerivedClass1的。


  4. 下面我们重点看看这个类DerivedDerivedClass,
  • 由外向内看,它并列地排布着继承而来的两个父类DerivedClass1与 DerivedClass2,还有自身的成员变量e。
  • DerivedClass1包含了它的成员变量c,以及Base,Base有一个0地址偏移的虚表指针,然后是成员变量a和b;
  • DerivedClass2的内存排布类似于DerivedClass1,注意到DerivedClass2里面竟然也有一份 Base。



    -这里有两份虚表了,分别针对DerivedClass1DerivedClass2

    &DerivedDericedClass_meta下方的数字0是首地址偏移量,

    靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}DerivedDerivedClass的内存偏移。

    这里的描述和这篇文章的描述有些出入。

8、虚继承

如果采用虚继承,像下面这样:

class DerivedClass1: virtual public Base{
int c;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
class DerivedClass2 : virtual public Base{
int d;
public:
void DerivedCommonFunction();
void virtual VirtualFunction();
};
class DerivedDerivedClass : public DerivedClass1, public DerivedClass2{
int e;
public:
void DerivedDerivedCommonFunction();
void virtual VirtualFunction();
};


  • Base类没有变化,但往下看:


  • DerivedClass1 就已经有变化了,原来是先排虚表指针与Base成员变量,vfptr位于0地址偏移处;但现在有两个虚表指针了,一个是vbptr,另一个是vfptr

    vbptr是这个DerivedClass1对应的虚表指针,它指向DerivedClass1的虚表vbtable,另一个vfptr是虚基类表对应的虚指针,它指向vftable

下面列出了两张虚表,第一张表是vbptr指向的表,8表示{vbptr}{vfptr}的偏移;

第二张表是vfptr指向的表,-8指明了这张表所对应的虚指针位于内存的偏移量。

DerivedClass2的内存分布类似于DerivedClass1,同样会有两个虚指针,分别指向两张虚表(第二张是虚基类表)。

下面来仔细看一下DerivedDerivedClass的内存分布,这里面有三个虚指针了,但base却只有一份。第一张虚表是内含DerivedClass1的,20表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,

第二张虚表是内含DerivedClass2 的,12表示它的虚指针{vbptr}离虚基表指针{vfptr}的距离,

最后一张表是虚基表,-20指明了它对应的虚指针{vfptr}在内存中的偏移。

虚继承的作用是减少了对基类的重复,代价是增加了虚表指针的负担(更多的虚表指针)。


9、总结

下面总结一下(当基类有虚函数时):

  1. 每个类都有虚指针和虚表;
  2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。

    有多少个虚函数,虚表里面的项就会有多少。

    多重继承时,可能存在多个的基类虚表与虚指针;
  3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,

    多重继承时虚基表与虚基表指针有且只有一份。

【转】C++类-内存分布的更多相关文章

  1. 记录:C++类内存分布(虚继承与虚函数)

    工具:VS2013 先说一下VS环境下查看类内存分布的方法: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存 ...

  2. 【C++ Primer | 15】C++类内存分布

    C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承.虚函数存在的情况下. 下面可以定义一个类,像下面这样: c ...

  3. 【转】C++类内存分布

    C++类内存分布  https://www.cnblogs.com/jerry19880126/p/3616999.html 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看 ...

  4. C++类内存分布

    http://www.cnblogs.com/jerry19880126/p/3616999.html#undefined 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看 ...

  5. 转载:C++类内存分布

    本文转自:http://www.cnblogs.com/jerry19880126/p/3616999.html,原文写的非常好,从中学到了虚继承的概念,也学会了用VS查看内存分布. 说下C++内存分 ...

  6. c++类内存分布解析

    首先使用Visual Studio工具查看类的内存分布,如下: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内 ...

  7. 使用Visual Studio查看C++类内存分布

    书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来看看编译器是怎么处理类成员内存分布的,特别是在继承.虚函数存在的情况下. 工欲善其事,必先利其器,我们先用好Visual Stu ...

  8. C++浅析——继承类内存分布和虚析构函数

    继承类研究 1. Code 1.1 Cbase, CTEST为基类,CTest2为其继承类,并重新申明了基类中的同名变量 class CBase { public: int Data; CBase() ...

  9. C++ 类的内存分布

    C++类内存分布 转自:http://www.cnblogs.com/jerry19880126/p/3616999.html   先写下总结,通过总结下面的例子,你就会明白总结了. 下面总结一下: ...

随机推荐

  1. Swift与Objective-C API的交互

    互用性是让 Swift 和 Objective-C 相接合的一种特性,使你能够在一种语言编写的文件中使用另一种语言.当你准备开始把 Swift 融入到你的开发流程中时,你应该懂得如何利用互用性来重新定 ...

  2. JavaScript中的字符串

    JavaScript字符串是JavaScript最重要的部分,可能比任何其他的数据类型都更多的用到. 所有的JavaScript对象共享的方法之一就是toString(). 字符串对象叫做String ...

  3. SQL Server(SSIS package) call .net DLL

    There are two method to call .net DLL in SQLSERVER. The first one is to use the sql clr but it has a ...

  4. Webform中Repeater控件--绑定嵌入C#代码四种方式

    网页里面嵌入C#代码用的是<% %>,嵌入php代码<?php ?> 绑定数据的四种方式: 1.直接绑定 <%#Eval("Code") %> ...

  5. linux kernel中timer的使用

    linux kernel中timer的使用 http://blog.csdn.net/njuitjf/article/details/16888821 在kernel中如果想周期性的干些什么事情,或者 ...

  6. windows7 安装python

    首先去Python官网,https://www.python.org 找到downloads,我这里系统是win7 x64,下载的是最新版本3.4.2 下载完成后有个msi文件,选择文件安装目录,一路 ...

  7. Mac Yosemite下Android Studio环境问题集合

    1. java not found 在mac Yosemite下,因jre升级到1.8,导致Android Studio无法启动.报错:"JAVA not found". 解决方法 ...

  8. 用jmeter进行多用户并发压力测试 [转]

    近日manager要求对项目进行压力测试,开始对jmeter进行了研究.jmeter是Apache一个开源项目,可对各种项目进行测试,甚至包括junit. 测试要求如下,多用户同时登陆web应用程序, ...

  9. Python每日一练(2):找出html中的所有链接(Xpath、正则两个版本)

    要在hrml文件中找出特定的内容,首先需要观察该内容是什么东西,在什么位置,这样才能找出来. 假设html的文件名称是:"1.html".href属性全都在a标签里. 正则版: # ...

  10. synapse socket总结三:心跳(Heartbeat)

    首先转载一篇关于心跳的博文解释: 所谓的心跳包就是客户端定时发送简单的信息给服务器端告诉它我还在而已.代码就是每隔几分钟发送一个固定信息给服务端,服务端收到后回复一个固定信息如果服务端几分钟内没有收到 ...