前言
之前阿里面试的时候有个面试官就问了我会不会"什么什么的内存模型",当时自己还不知道这个名词(知道概念,但确确实实不知道叫这个名字.....),所以就回了是问关于大小端存储么?面试官就问下一个问题了.....
后来在《程序员的自我修养》这本书中,看了相关的概念,在这里整理一下:
Visual Studio查看虚函数表
在这里首先插一个话题,讲解一下如何查看虚函数表。
我们通过调试去查看变量的分布的时候,会发现只能显示出来基类的虚函数表,而派生类的虚函数表却是被隐藏的;我们想查看这个怎么办?下面是步骤:
先选择左侧的C/C++->命令行,然后在其他选项这里写上/d1 reportAllClassLayout,它可以看到所有相关类的内存布局,如果写上/d1 reportSingleClassLayoutXXX(XXX为类名),则只会打出指定类XXX的内存布局。近期的VS版本都支持这样配置。
运行程序的话就会自动生成一张虚函数表了:
这个内存结构图分成了两个部分,上面是内存分布,下面是虚表;就可以简单进行查看了。
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.构造一个空类:
这里空类的长度却是1,是为了用来标识该对象;
2.我们在类中添加成员变量:
这个涉及到了内存对齐问题,之前自己写过一篇博客说过这个概念。调试看一下:
3.只有虚函数的类:
内存中虚函数表占了4个字节,而构建的虚函数表在我的这一篇博客中也讲到了。
4.有成员变量与虚函数的类
就是将情况2、3加起来就行了。
单一继承(含成员变量+虚函数+虚函数覆盖)
继承关系:
通过代码查看的虚函数表是这样的:
构建的虚函数表是这样的:
多继承(含成员函数+虚函数+虚函数覆盖)
继承关系:
三个int型,2个虚函数表,所以长度为20;虚函数表是这个样子:
内存布局是这样:
深度为2的继承(成员变量+虚函数+虚函数覆盖)
继承关系:
4个int型,2个虚函数表;代码显示的类的布局是这样:
内存布局:
如果自己手动计算一下继承的内容,会发现对两张虚函数表的内容感到奇怪,比如顺着CGrandChildren
的CParent1
的虚函数表应该有:f0,g0,h0,g1,h1,h2,f2,f3
,但是我们发现剩下的却只有f0,g0,h0,h2,f2,f3
,g1,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; };
};
显示的内存分布是这样的:
重复继承(含成员变量+虚函数+虚函数覆盖)
继承关系:
这样的继承关系在内存分布中是这样的:
由于基类中的m_nAge在内存分布中出现了两次,所以最后的结果是5个int类型和2个虚函数表,共计28字节。
内存布局是这样的:
单一虚继承(含成员变量+虚函数+虚函数覆盖)
继承关系如下:
所谓的虚继承就是把继承语法前加上virtual
关键字,例如class B:virtual public A{..};
虚拟继承的出现就是为了解决重复继承中多个间接父类的问题的 。内存分布是这样的:
这里需要解释下,因为出现了vfptr
与vbptr
,前面的我们已经经常看到了,但是vbptr
却是第一次见,它是CChildren
对应的虚表指针,它指向CChildren
的虚表vtable,另一个vfptr
位于0地址偏移处,它指向vftable。从截图中也可以看出有两个表vftable
与vbtable
。第二张vbtable
中的8表示vbptr
与基类的vfptr
之间的偏移。
内存布局为:
另外提及一下,如果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; };
};
内存分布为:
多虚继承(含成员变量+虚函数+虚函数覆盖)
(1)继承关系如下:
[图片上传失败...(image-1b91a3-1540987989826)]
其中CParent1是虚继承,CParent2是一般继承。
内存分布为:
内存布局:
(2)再看另一种继承关系:
其中CParent2是虚继承,CParent1是一般继承。
内存分布为:
内存布局为:
(3)继承关系:
内存分布为:
从这里可以看出vbtable确实是存储了指向相应的基类的虚函数表指针。
内存布局为:
钻石型的虚拟多重继承(含成员变量+虚函数+虚函数覆盖)
继承关系:
内存分布为:
内存布局为: