解析C++类内存分布

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

先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。

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

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

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

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

从这里可以看到普通类的排布方式,成员变量依据声明的顺序进行排列(类内偏移为0开始),成员函数不占内存空间。

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

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

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

可以看到子类继承了父类的成员变量,在内存排布上,先是排布了父类的成员变量,接着排布子类的成员变量,同样,成员函数不占字节。

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

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

这个内存结构图分成了两个部分,上面是内存分布,下面是虚表,我们逐个看。VS所带编译器是把虚表指针放在了内存的开始处(0地址偏移),然后再是成员变量;下面生成了虚表,紧跟在&Base1_meta后面的0表示,这张虚表对应的虚指针在内存中的分布,下面列出了虚函数,左侧的0是这个虚函数的序号,这里只有一个虚函数,所以只有一项,如果有多个虚函数,会有序号为1,为2的虚函数列出来。

编译器是在构造函数创建这个虚表指针以及虚表的。

那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。

所以,如果是调用Base *p = new Derived();生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由Derived*到Base*的转换并没有改变虚表指针,所以这时候p->VirtualFunction,实际上是p->vfptr->VirtualFunction,它在构造的时候就已经指向了子类的VirtualFunction,所以调用的是子类的虚函数,这就是多态了。

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

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

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

上半部是内存分布,可以看到,虚表指针被继承了,且仍位于内存排布的起始处,下面是父类的成员变量a和b,最后是子类的成员变量c,注意虚表指针只有一个,子类并没有再生成虚表指针了;下半部的虚表情况与父类是一样的。

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

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

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

还是只有一个虚表指针,但是下方虚表的内容变化了,虚表的0号是父类的VirtualFunction,而1号放的是子类的VirtualFunction2。也就是说,如果定义了DerivedClass的对象,那么在构造时,虚表指针就会指向这个虚表,以后如果调用的是VirtualFunction,那么会从父类中寻找对应的虚函数,如果调用的是VirtualFunction2,那么会从子类中寻找对应的虚函数。

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

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

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

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

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

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

class DerivedClass2 : 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中有一个虚表指针,地址偏移为0

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

DerivedClass2的情况是类似于DerivedClass1的。

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

这里有两份虚表了,分别针对DerivedClass1与DerivedClass2,在&DerivedDericedClass_meta下方的数字是首地址偏移量,靠下面的虚表的那个-16表示指向这个虚表的虚指针的内存偏移,这正是DerivedClass2中的{vfptr}在DerivedDerivedClass的内存偏移。

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

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}在内存中的偏移。

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

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

1. 每个类都有虚指针和虚表;

2. 如果不是虚继承,那么子类将父类的虚指针继承下来,并指向自身的虚表(发生在对象构造时)。有多少个虚函数,虚表里面的项就会有多少。多重继承时,可能存在多个的基类虚表与虚指针;

3. 如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。

以上就是解析C++类内存分布的详细内容,更多关于C++类内存分布的资料请关注我们其它相关文章!

(0)

相关推荐

  • 关于C/C++内存管理示例详解

    1.内存分配方式 在C++中,内存分成五个区,分别是堆.栈.自由存储区.静态存储区和常量存储区. 1) 栈 执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放.栈内存分配运算内置处理器指令集中,效率很高,但分配的内存容量有限. 2) 堆 由new分配的内存块,释放由程序员控制.如果程序员没有释放,那么就在程序结束的时候,被操作系统回收. 3) 自由存储区 由malloc等分配的内存块,用free结束自己的生命. 4) 静态存储区 全局变量和静态变量被分配到

  • C++使用动态内存分配的原因解说

    上节我们讲了C++程序的内存分布.C++程序的内存分布 本节来介绍为什么要进行内存分配. 按需分配,根据需要分配内存,不浪费. 内存拷贝函数void* memcpy(void* dest, const void* src, size_t n); 从源src中拷贝n字节的内存到dest中.需要包含头文件#include <string.h> #include <stdio.h> #include <string.h> using namespace std; int ma

  • c++ 排查内存泄漏的妙招

    前言 对于c++而言,如何查找内存泄漏是程序员亘古不变的话题:解决之道可谓花样繁多.因为最近要用到QT写程序,摆在我面前的第一个重要问题是内存防泄漏.如果能找到一个简单而行之有效的方法,对后续开发大有裨益.久思终得诀窍,本文就详细介绍我对此问题的应对之策.(文末符完整代码) 如何判断内存有泄漏 内存分配和释放对应的操作是new.delete.如何判断内存是否释放干净?其实判断起来非常简单:一个独立的模块整个生存周期内new的个数和delete的个数相等.用伪代码标示如下: int newCoun

  • c++ 结构体内存对齐基本概念及示例

    基本概念: 各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数, 各成员变量在存放的时候根据在结构中出现的顺序依次申请空间 同时按照上面的对齐方式调整位置. 空缺的字节自动填充, 同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大的空间的类型的字节数)的倍数,所以在为最后一个成员变量申请空间后 还会根据需要自动填充空缺的字节: 举例说明: #include <iostream> using namespace std; #pragma pack

  • c++ 动态内存分配相关总结

    下面随笔是关于c++动态内存分配. 动态申请内存操作符 new new 类型名T(初始化参数列表) 功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值. 结果值:成功:T类型的指针,指向新分配的内存:失败:抛出异常. 释放内存操作符delete delete 指针p 功能:释放指针p所指向的内存.p必须是new操作的返回值. //例1 动态创建对象举例 #include <iostream> using namespace std; class Point { pub

  • C++ 内存分区模型的使用(代码区、全局区、栈区、堆区、new)

    内存分区模型 1 代码区 2 全局区 // 全局变量.静态变量.常量 #include <iostream> using namespace std; // 全局变量.静态变量.常量 //全局变量 int g_a=10; int g_b=10; //const修饰的全局常量 const int c_g_a = 10; const int c_g_b = 10; int main() { //创建普通局部变量 int a = 10; int b = 10; cout << "

  • C++ 动态内存分配详解(new/new[]和delete/delete[])

    一.为什么需要动态内存分配? 在C++程序中,所有内存需求都是在程序执行之前通过定义所需的变量来确定的. 但是可能存在程序的内存需求只能在运行时确定的情况. 例如,当需要的内存取决于用户输入. 在这些情况下,程序需要动态分配内存,C ++语言将运算符new和delete合成在一起. (1)特点 1.C++中通过new关键字进行动态内存申请 2.C++中的动态内存分配是基于类型进行的 3.delete关键字用于内存释放 (2)语法 ①变量申请: Type* pointer = new Type;

  • 详解C++ 内存对齐

    操作系统64位和32位有什么区别? 64位操作系统意味着其cpu拥有更大的寻址能力.理论上来说,其性能相比于32位操作系统会提升1倍.但是这也需要在64位操作系统上运行的软件也是64位的. 软件中数据类型的的字节数大小其实和操作系统是多少位的没有关系,而是由编译器决定的.也就是说数据结构占多少位取决于在软件编译时我们选择的是64位还是32位的编译器.其具体占位数在编译器已经决定了. 数据类型对应字节数 下面是不同位数编译器下基本数据类型对应的字节数. 32位编译器: char :1个字节 cha

  • 关于C#调用C++dll传指针释放内存问题

    一.传入dll前,在C#中申请内存空间 c#里面的指针即 IntPtr 申请如下: IntPtr SrcImgData = Marshal.AllocHGlobal(length); 这种需要提前知道空间大小,否则无法确定空间大小,会导致dll内部处理时越界报错. c#里面申请空间了,那么c++里面一般就是在这些空间里面操作了,一般不会重新分配内存,那么就不需要加引用了. c++: uchar* SrcImg c#导入dll函数时申明: IntPtr SrcImg 那么内存释放自然也是由c#来进

  • 解析C++类内存分布

    工欲善其事,必先利其器,我们先用好Visual Studio工具,像下面这样一步一步来: 先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局.近期的VS版本都支持这样配置. 下面可以定义一个类,像下面这样: class Base { int a; int b; public: void Com

  • C++ 类中有虚函数(虚函数表)时 内存分布详解

    虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数.这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数. 这里我们着重看一下这张虚函数表.C++的编译器应该是

  • C++虚函数表与类的内存分布深入分析理解

    目录 不可定义为虚函数的函数 将析构函数定义为虚函数的作用 虚函数表原理 继承关系中虚函数表结构 多重继承的虚函数表 多态调用原理 对齐和补齐规则 为什么要有对齐和补齐 资源链接 不可定义为虚函数的函数 类的静态函数和构造函数不可以定义为虚函数: 静态函数的目的是通过类名+函数名访问类的static变量,或者通过对象调用staic函数实现对static成员变量的读写,要求内存中只有一份数据.而虚函数在子类中重写,并且通过多态机制实现动态调用,在内存中需要保存不同的重写版本. 构造函数的作用是构造

  • Java内存分布归纳整理详解

    Java内存分布:Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域:方法区.虚拟机栈.本地方法栈.堆.程序计数器. 1.程序计数器 程序计数器是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器.在虚拟机的概念模型中,字节码解释器工作时就是通过改变这个计数器的值来选取吓一条需要执行的字节码指令. 分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成. 为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

  • C++对象内存分布详解(包括字节对齐和虚函数表)

    1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数表,虚函数表在编译阶段就已经生成,同类的不同对象中的虚函数指针指向同一个虚函数表,不同类对象的虚函数指针指向不同虚函数表. 2.何时进行动态绑定: (1)每个类对象在被构造时不用去关心是否有其他类从自己派生,也不需要关心自己是否从其他类派生,只要按照一个统一的流程:在自身的构造函数执行之前把自己所属类(即当前构造函数所属的类)的虚函数表的地址绑定到当前对象上(一般是保存在对象内存空间

  • 浅谈C++对象的内存分布和虚函数表

    c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员. 2.静态数据成员被提取出来放在程序的静态数据区内,为该类所有对象共享,因此只存在一份. 3.静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类所有对象共享,因此每一个成员函数也只能存在一份代码实体.在c++中类的成员函数都是保存在静态存储区中的 ,那静态函数也是保存在静态存储区中的,他们都是在类中保存同一个惫份. 因此,构成对象本身的只

  • 再也不用怕! 让你彻底搞明白Java内存分布

    一.堆内内存 堆内内存分为三大部分,年轻代 , 老年代 和 元空间,所以 堆内内存 = 年轻代 + 老年代 + 元空间,下面细聊下三部分 1.1 年轻代-Young Generation 存放的是new 生成的对象 年轻代是为了尽可能快速的回收掉那些生命周期短的对象 Eden 大部分对象在Eden区中生成 当Eden区满时,会做一次young gc, 依然存活的对象将被复制到Survivor区, 当一个Survivor 区满时, 此区的存活对象将被复制到另外一个Survivor区 Survivo

  • C++内存分布及用法

    目录 一.内存基础 1.内存分布 2.栈区与堆区的区别 二.内存分配 1.内存分配方式 2.new的用法 3.delete用法 4.new与malloc的区别 5.内存泄漏 三.内存拓展 1.内存概念 2.虚拟内存 四.思考 1.代码中的b属于栈区还是堆区? 一.内存基础 1.内存分布 通过下面一张图看看C++的内存分布: 栈区:由编译器自动分配与释放,存放为程序运行时函数分配的局部变量.函数参数:栈内存分配运算内置于处理器的指令集中,效率很高,但是分配内存的容量有限: 堆区:由new.mall

  • python命令行参数解析OptionParser类用法实例

    本文实例讲述了python命令行参数解析OptionParser类的用法,分享给大家供大家参考. 具体代码如下: from optparse import OptionParser parser = OptionParser(usage="usage:%prog [optinos] filepath") parser.add_option("-t", "--timeout", action = "store", type =

  • java实现字符串四则运算公式解析工具类的方法

    项目中用到用户定义运算公式进行就算的需求,这样需要进行字符串四则运算解析,下面提供字符串公式四则运算解析与计算工具类,需要的同学可参考. 工具类如下:FormulaCalculator.java: package org.nercita.bcp.record.util; import java.util.ArrayList; import java.util.LinkedList; /** * @author zhangwenchao * @since 2016-08-26 * 公式计算的工具类

随机推荐