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

posted @ 2018-10-30 14:01  MrYun  阅读(19163)  评论(5编辑  收藏  举报