• C++内存模型


    前言

    之前阿里面试的时候有个面试官就问了我会不会"什么什么的内存模型",当时自己还不知道这个名词(知道概念,但确确实实不知道叫这个名字.....),所以就回了是问关于大小端存储么?面试官就问下一个问题了.....
    后来在《程序员的自我修养》这本书中,看了相关的概念,在这里整理一下:

    Visual Studio查看虚函数表

    在这里首先插一个话题,讲解一下如何查看虚函数表。

    我们通过调试去查看变量的分布的时候,会发现只能显示出来基类的虚函数表,而派生类的虚函数表却是被隐藏的;我们想查看这个怎么办?下面是步骤:

    image.png

    image.png

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

    运行程序的话就会自动生成一张虚函数表了:

    image.png

    这个内存结构图分成了两个部分,上面是内存分布,下面是虚表;就可以简单进行查看了。

    C++内存模型(内存布局)

    内存区域

    这部分经友人提醒,可以从C++标准的"内存"概念中出发,后面会更新这部分内容。
    HERE
    C++内存分为5个区域:
    堆 heap :
    由new分配的内存块,其释放编译器不去管,由我们程序自己控制(一个new对应一个delete)。如果程序员没有释放掉,在程序结束时OS会自动回收。涉及的问题:“缓冲区溢出”、“内存泄露”
    栈 stack :
    是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
    存放在栈中的数据只在当前函数及下一层函数中有效,一旦函数返回了,这些数据也就自动释放了。
    全局/静态存储区 (.bss段和.data段) :
    全局和静态变量被分配到同一块内存中。在C语言中,未初始化的放在.bss段中,初始化的放在.data段中;在C++里则不区分了。
    常量存储区 (.rodata段) :
    存放常量,不允许修改(通过非正当手段也可以修改)
    代码区 (.text段) :
    存放代码(如函数),不允许修改(类似常量存储区),但可以执行(不同于常量存储区)
    根据C++对象生命周期不同,C++的内存模型有三种不同的内存区域:
    1.自由存储区,动态区、静态区局部非静态变量的存储区域(栈)
    2.动态区:用operator new,malloc分配的内存(堆)
    3.静态区:全局变量、静态变量、字符串常量存在位置

    内存布局

    介绍完了内存区域,那么在C++中类对象的内存布局是如何分布的呢?
    回顾一下,我们写class的时候,会有成员变量、成员函数、静态成员变量、静态成员函数、虚函数与纯虚函数这几个元素,他们都分布在内存中,后文会详细介绍这些分布;在这里,影响对象大小的有哪些因素呢?成员变量的类型与数量、虚函数表的指针(_vftptr)、虚基类表指针(_vbtptr)-->产生虚函数表、单一继承、多重继承、重复继承、虚拟继承,当然也会有编译器的优化与内存对齐的影响,不过这里重点讲一下类的成员变量与虚函数表相关的内存布局。

    单一类

    1.构造一个空类:

    image.png

    这里空类的长度却是1,是为了用来标识该对象;

    2.我们在类中添加成员变量:

    image.png

    这个涉及到了内存对齐问题,之前自己写过一篇博客说过这个概念。调试看一下:

    image.png

    3.只有虚函数的类:

    image.png

    内存中虚函数表占了4个字节,而构建的虚函数表在我的这一篇博客中也讲到了。

    image.png

    4.有成员变量与虚函数的类

    image.png

    就是将情况2、3加起来就行了。

    单一继承(含成员变量+虚函数+虚函数覆盖)
    继承关系:
    image.png

    通过代码查看的虚函数表是这样的:

    image.png

    构建的虚函数表是这样的:

    image.png

    多继承(含成员函数+虚函数+虚函数覆盖)

    继承关系:

    image.png

    三个int型,2个虚函数表,所以长度为20;虚函数表是这个样子:

    image.png

    内存布局是这样:

    image.png

    深度为2的继承(成员变量+虚函数+虚函数覆盖)

    继承关系:

    image.png

    4个int型,2个虚函数表;代码显示的类的布局是这样:

    image.png

    内存布局:

    image.png

    如果自己手动计算一下继承的内容,会发现对两张虚函数表的内容感到奇怪,比如顺着CGrandChildrenCParent1的虚函数表应该有:f0,g0,h0,g1,h1,h2,f2,f3,但是我们发现剩下的却只有f0,g0,h0,h2,f2,f3g1,h1都在CParent2这个表里。所以,如果在第二个基类中有的虚函数,在深度为2的继承的第一个基类的虚函数表中需要排除这些虚函数。简单的一个记忆方法就是按照当前方法计算出虚函数,然后再检查其他基类中有没有这个虚函数,如果有的话就删掉;如果深度为1的派生类里有新的虚函数的话(不是重构基类的虚函数),会在第一张表里生成。当然这也只是大学期间自己做题的小技巧,其原理是这样的:重构的话必须找到相对应的基类虚函数,而在第二个基类中的虚函数只能在第二个虚函数表才能找到;此外,虚函数表会优先生成新的虚函数在第一次遇见的时候。下面写一段代码验证下:

    class A {
    public:
    	virtual void f1() { cout << "A:f1" << endl; };
    	virtual void f2() { cout << "A:f2" << endl; };
    	virtual void f3() { cout << "A:f3" << endl; };
    };
    
    class B {
    public:
    	virtual void g1() { cout << "B:g1" << endl; };
    	virtual void g2() { cout << "B:g2" << endl; };
    	virtual void f2() { cout << "B:f2" << endl; };
    };
    
    class C :public A, public B {
    	virtual void f1() { cout << "C:f1" << endl; };
    	virtual void g1() { cout << "C:g1" << endl; };
    };
    
    class D :public C {
    	virtual void f1() { cout << "D:f1" << endl; };
    	virtual void g2() { cout << "D:g2" << endl; };
    };
    

    显示的内存分布是这样的:

    image.png

    重复继承(含成员变量+虚函数+虚函数覆盖)

    继承关系:

    image.png

    这样的继承关系在内存分布中是这样的:

    image.png

    由于基类中的m_nAge在内存分布中出现了两次,所以最后的结果是5个int类型和2个虚函数表,共计28字节。

    内存布局是这样的:

    image.png

    单一虚继承(含成员变量+虚函数+虚函数覆盖)

    继承关系如下:

    image.png

    所谓的虚继承就是把继承语法前加上virtual关键字,例如class B:virtual public A{..};

    虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的 。内存分布是这样的:

    image.png

    这里需要解释下,因为出现了vfptrvbptr,前面的我们已经经常看到了,但是vbptr却是第一次见,它是CChildren对应的虚表指针,它指向CChildren的虚表vtable,另一个vfptr位于0地址偏移处,它指向vftable。从截图中也可以看出有两个表vftablevbtable。第二张vbtable中的8表示vbptr与基类的vfptr之间的偏移。

    内存布局为:

    image.png

    另外提及一下,如果CChildren里全部是重载基类中的虚函数的话,或者说没有新的虚函数的话,vftptr指向的虚函数表就是空的,所以计算大小的时候可以不用算进去,因为实际上并没有创建相应的表格:

    举个例子:

    class A {
    public:
    	virtual void f1() { cout << "A:f1" << endl; };
    	virtual void f2() { cout << "A:f2" << endl; };
    	virtual void f3() { cout << "A:f3" << endl; };
    };
    
    class B:virtual A {
    public:
    	//virtual void g1() { cout << "B:g1" << endl; };
    	virtual void f2() { cout << "B:f2" << endl; };
    	virtual void f3() { cout << "B:f3" << endl; };
    };
    

    内存分布为:

    image.png

    多虚继承(含成员变量+虚函数+虚函数覆盖)

    (1)继承关系如下:

    1540985431636

    其中CParent1是虚继承,CParent2是一般继承。

    内存分布为:

    image.png

    内存布局:

    image.png

    (2)再看另一种继承关系:

    image.png

    其中CParent2是虚继承,CParent1是一般继承。

    内存分布为:

    image.png

    内存布局为:

    image.png

    (3)继承关系:

    image.png

    内存分布为:

    image.png

    从这里可以看出vbtable确实是存储了指向相应的基类的虚函数表指针。

    内存布局为:

    image.png

    钻石型的虚拟多重继承(含成员变量+虚函数+虚函数覆盖)

    继承关系:

    image.png

    内存分布为:

    image.png

    内存布局为:

    image.png

  • 相关阅读:
    动态获取Resources里面的图片列表
    在LINQ中实现多条件联合主键LEFT JOIN
    Failed to fetch URL http://dlssl.google.com/android/repository/repository.xml
    LINQ多条件OR模糊查询
    使用ILMerge将所有引用的DLL和exe文件打成一个exe文件
    安卓模拟器、电脑运行安卓系统android、apk文件
    C/C++中枚举类型(enum)
    指针详解
    Visaul Studio 2008 中配置DirectX9c的开发环境
    出自涅磐,缘自凤凰
  • 原文地址:https://www.cnblogs.com/yunlambert/p/9876491.html
Copyright © 2020-2023  润新知