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)继承关系如下:

[图片上传失败...(image-1b91a3-1540987989826)]

其中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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容