C++内存对齐及内存布局

前言

  本文会展示内存对齐,及继承、虚继承等各个情况下内存的布局,并根据结果总结使用场景。

基本调试方法

  使用编译器自带的工具,在Visual Studio下,右键解决方案,在弹出的菜单下,点击属性:

  在属性页中,依次找到 配置属性 》C/C++ 》命令行,在【其他选项】中输入/d1 reportAllClassLayout,点击确定。
  随后重新编译(重新生成解决方案),就可以在输出栏看到内存的布局(输出不少,可以点击输出栏后,用CTRL+F查找):
  当然,查看单个类也是可以的,只需要输入:/d1 reportSingleClassLayout○○○,用类名直接代替最后的○○○即可。

一、内存对齐

1.单个变量

  C++普通类占用内存的只有成员变量,普通成员函数不占用类的内存空间:

class TEST{
    int a;
};
//class TEST    size(4):
//  +---
// 0    | a
//  +---

2.多个变量

class TEST{
    double d;
    int a;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//      | <alignment member> (size=4)
//  +---

  C++默认对齐大小为类内最大的基础类型大小,因此int变量后多出4个字节的对齐空间。

3.多变量实验

class TEST{
    char c;
    int a;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | c
//      | <alignment member> (size=3)
// 4    | a
// 8    | d
//  +---
class TEST{
    int a;
    char c;
    double d;
};
//class TEST    size(16):
//  +---
// 0    | a
// 4    | c
//      | <alignment member> (size=3)
// 8    | d
//  +---
class TEST{
    double d;
    int a;
    char c;
};
//class TEST    size(16):
//  +---
// 0    | d
// 8    | a
//12    | c
//      | <alignment member> (size=3)
//  +---

  这三个都没有变化,但假如:

class TEST{
    char c;
    double d;
    int a;
};
//class TEST    size(24):
//  +---
// 0    | c
//      | <alignment member> (size=7)
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

4.内存对齐规则

  发生了变化,多了8个字节,由此得到规律:
  想像一个表格,列数为拥有最大基础类型长度,例如上面的double,长度为8字节,则每行8列:

对齐\字节 1 2 3 4 5 6 7 8
0
8
16

  将变量从上到下填充,塞入当前变量时,如果能在当前行塞下,就塞入,塞不下,就另起一行再塞入:

对齐\字节 1 2 3 4 5 6 7 8
0 char
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8
16 int_1 int_2 int_3 int_4

  同时,每个变量只会放在整[自己大小的]的字节处,double只会从整8字节开始,int只会从整4字节开始,short只会从整2字节开始,char就随意放置,上面多变量实验第一例的内存布局就如下图所示:

对齐\字节 1 2 3 4 5 6 7 8
0 char int_1 int_2 int_3 int_4
8 double_1 double_2 double_3 double_4 double_5 double_6 double_7 double_8

数组成员

  数组成员相当于在当前位置直接定义当前数组长度个变量,对齐长度不会变成数组占用的字节数。

非基础类型成员

struct Box {
    double a;
    char b;
};
class TEST{
    Box b;
    char c;
};
//class Box size(16):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=7)
//  +---
//
//class TEST    size(40):
//  +---
// 0    | Box b
//16    | c
//      | <alignment member> (size=7)
//  +---

  相当于直接把成员对象空间堆在里面,同时持有类的对齐也会受到成员对象的影响,上例TEST的对齐大小受Box的影响,变成了8

5.更大的对齐

  看起来内存对齐的最大对齐就是8,毕竟我们所熟知的类型中,只有double才会占用8字节,非基础类型成员的大小也不会直接作用于内存对齐。
  不过更大的基础类型也是存在的,就比如在SIMD类型中_m128和_m256分别会占用16字节和32字节,并且它们的空间占用会导致内存对齐数的增大。如果你的类中只有_m256和char的变量各一个,用上面的方法,会看到char后面会跟着31个内存对齐空间。
  这两个类型分别在nmmintrin.himmintrin.h中,相关操作可以查阅SIMD的资料。

6.结论

  不同的变量排列方式会改变对象的大小,建议变量从大到小或从小到大排列,这样空间会达到最优。
  学会内存的对齐有什么意义呢?从上例来看,好像只能优化对象的布局,使其占用空间更小。在当今的计算机,我们普遍不关注几个字节的空间占用,一个用途,就是如果数据使用结构化存储,当数据量十分庞大,成千上万,乃至千万、上亿时,省下的空间能肉眼可见;不过这种存储技术通常都有相关优化,一般也不是我们所关心的。
  不过我在编写代码时,遇到了一定要内存对齐的情况,是编写SIMD(单指令流多数据流)时遇到的,详情可见SIMD类型堆上分配方法探究

7.其他有关内存布局的C++特性与函数

①_declspec关键字
  __declspec用于指定所给定类型的实例的与Microsoft相关的存储方式,通常用法是__declspec(表达式),当表达式为align(n)时(n为2的整次幂),可设置对象地址对齐:

_declspec(align(16)) class TEST {
    char c;
};
//class TEST    size(16):
//  +---
// 0    | c
//  +---

  可以看到对象最小大小为16,不过最终大小可以不为16的倍数,对齐地址和对齐内存块还是有区别的,这种方式能对齐静态存储,但动态分配的内存不能保证对齐地址。
②alignas、alignof关键字
  C++11标准的关键字。
  alignof可在运行时得到类型的对齐值:

_declspec(align(4)) class TEST {
    double a;
};
int main() {
    std::cout << alignof(TEST);//output: 8
_declspec(align(16)) class TEST {
    double a;
};//output: 16

  alignas可在定义变量时,改变当前变量的对齐值:

_declspec(align(16)) class TEST {
    double a;
    double b;
    alignas(32) char c;
};
int main() {
    std::cout << alignof(TEST);//output: 32
    system("pause");
}
//class TEST    size(64):
//  +---
// 0    | a
// 8    | b
//      | <alignment member> (size=16)
//32    | c
//      | <alignment member> (size=31)
//  +---

③_mm_malloc, _mm_free函数
  SIMD库带的函数,用于分配地址对齐的动态内存,常常和placement new配合使用,使用事例参照上面那个遇到的SIMD坑。

二、继承、带有虚函数、虚继承的内存布局

1.单个类

  带有一个或多个虚函数,将得到一个虚表指针vfptr:

class TEST{
    int a;
    virtual bool func() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---

  虚表指针vfptr会指向存有虚函数的虚表,虚表本身大小我们不必考虑。
  vfptr会放到成员变量前,单单看这个例子,vfptr的大小是4,不过如果增加一个更大的变量:

class TEST{
    double d;
    int a;
    virtual bool func() {}
};
//class TEST    size(24):
//  +---
// 0    | {vfptr}
// 8    | d
//16    | a
//      | <alignment member> (size=4)
//  +---

  在x64平台上vfptr大小变成了8,在x86平台上,大小还是4,但内存会对齐四个,最终占用大小还是8;如果是SIMD类型,vfptr的大小甚至可能是16、32。

2.继承

  此时出现继承,有四种情况,有无虚函数、有无虚继承的两两组合:

  • 无虚继承,无虚函数
class TEST2 :  public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 8    | | d
//16    | | a
//  | | <alignment member> (size=4)
//  | +---
//24    | a2
//      | <alignment member> (size=4)
//  +---

  直接把父类的内存放到自己的最前面,同时内存布局继承父类的(父类有double,内存对齐为8,子类也对齐为8),和上面的对象成员方式基本一致。

  • 无虚继承,有虚函数
class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

  生成结果和无虚继承,无虚函数等同,没有变化(没出现虚表指针vfptr),推测为:和父类共用虚表指针。

  • 虚继承,无虚函数
class TEST2 :  virtual public TEST {
    int a2;
};
//class TEST2   size(32):
//  +---
// 0    | {vbptr}
// 4    | a2
//  +---
//  +--- (virtual base TEST)
// 8    | {vfptr}
//16    | d
//24    | a
//      | <alignment member> (size=4)
//  +---

  在自身成员变量前,增加指向父类的虚指针(vbptr),这个虚指针的大小和对齐不与父类相同,但如果自身用更大的基础变量,同样会导致vbptr向更大变量对齐:

class TEST2 :  virtual public TEST {
    int a2;
    double d2;
};
//class TEST2   size(48):
//  +---
// 0    | {vbptr}
// 8    | a2
//      | <alignment member> (size=4)
//16    | d2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//32    | d
//40    | a
//      | <alignment member> (size=4)
//  +---

  图中可见,TEST2的vbptr的大小变成了8。

  • 虚继承,有虚函数
class TEST2 :  virtual public TEST {
    int a2;
    double d2;
    virtual void func2() {}
};
//class TEST2   size(56):
//  +---
// 0    | {vfptr}
// 8    | {vbptr}
//16    | a2
//      | <alignment member> (size=4)
//24    | d2
//  +---
//  +--- (virtual base TEST)
//32    | {vfptr}
//40    | d
//48    | a
//      | <alignment member> (size=4)
//  +---

  在父类虚指针vbptr前,再次出现了自己指向虚表的虚指针vfptr,其大小变化方式和vbptr相同,在这个有double类型的TEST2中,大小都变成了8。

3.总结

三、钻石继承结构的内存布局

1.一把梭式的直接继承

class TEST{
    int a;
    virtual bool func() {}
};

class TEST2 :  public TEST {
    int a2;
    virtual void func2() {}
};

class TEST3 : public TEST {
    int a3;
    virtual void func3() {}
};

class TEST4 : public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST    size(8):
//  +---
// 0    | {vfptr}
// 4    | a
//  +---
//class TEST2   size(12):
//  +---
// 0    | +--- (base class TEST)
// 0    | | {vfptr}
// 4    | | a
//  | +---
// 8    | a2
//  +---
//TEST3与TEST2一致
//.....
//class TEST4   size(28):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | +--- (base class TEST)
// 0    | | | {vfptr}
// 4    | | | a
//  | | +---
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---

  和前面说的一样,都是直接堆放,可以发现,TEST2和TEST3的空间中,各有一个TEST。

2.各层虚继承

  我们将上图从上到下分为1,2,3三层。

①2对1层单个虚继承实验
class TEST2 : virtual public TEST {
    int a2;
    virtual void func2() {}
};
//其他不变
//class TEST2   size(20):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a2
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | +--- (base class TEST)
//12    | | | {vfptr}
//16    | | | a
//  | | +---
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

  TEST2变成了前面虚继承,有虚函数的情况,而TEST4因为未采取虚继承,依旧是直接把TEST2直接放在前面

②2对1层双虚继承实验
//对TEST3做与TEST2相同的变化,既变为虚继承
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST2)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a2
//  | +---
//12    | +--- (base class TEST3)
//12    | | {vfptr}
//16    | | {vbptr}
//20    | | a3
//  | +---
//24    | a4
//  +---
//  +--- (virtual base TEST)
//28    | {vfptr}
//32    | a
//  +---

  TEST4的粗暴堆放没有变化,但TEST只有一个了!

③2对1层全虚继承,3对2层单虚继承
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};
//class TEST4   size(36):
//  +---
// 0    | +--- (base class TEST3)
// 0    | | {vfptr}
// 4    | | {vbptr}
// 8    | | a3
//  | +---
//12    | a4
//  +---
//  +--- (virtual base TEST)
//16    | {vfptr}
//20    | a
//  +---
//  +--- (virtual base TEST2)
//24    | {vfptr}
//28    | {vbptr}
//32    | a2
//  +---

  TEST4只堆放TEST3,TEST2不再被堆放,但TEST4的父节点指针vbptr和虚表指针vfptr都未出现。

④全虚继承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

  TEST4的vfptr和vbptr同时出现了!

⑤3对2层全虚继承,2对1层非全虚继承
//class TEST4   size(44):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a4
//  +---
//  +--- (virtual base TEST2)
//12    | +--- (base class TEST)
//12    | | {vfptr}
//16    | | a
//  | +---
//20    | a2
//  +---
//  +--- (virtual base TEST)
//24    | {vfptr}
//28    | a
//  +---
//  +--- (virtual base TEST3)
//32    | {vfptr}
//36    | {vbptr}
//40    | a3
//  +---

  TEST2再次堆放TEST空间,并且TEST3也指向一个TEST空间,可以看出2对1层未全虚继承,就无法消除存在多个TEST的歧义。

⑥更多继承

  如果有后续的继承结构,例如下面:
class TEST4 : virtual public TEST2, public TEST3 {
    int a4;
    virtual void func4() {}
};

class TEST5 : public TEST3 {
    int a5;
    virtual void func5() {}
};

class TEST6 : virtual public TEST4, virtual public TEST5 {
    int a6;
    virtual void func6() {}
};
//class TEST6   size(64):
//  +---
// 0    | {vfptr}
// 4    | {vbptr}
// 8    | a6
//  +---
//  +--- (virtual base TEST)
//12    | {vfptr}
//16    | a
//  +---
//  +--- (virtual base TEST2)
//20    | {vfptr}
//24    | {vbptr}
//28    | a2
//  +---
//  +--- (virtual base TEST4)
//32    | +--- (base class TEST3)
//32    | | {vfptr}
//36    | | {vbptr}
//40    | | a3
//  | +---
//44    | a4
//  +---
//  +--- (virtual base TEST5)
//48    | +--- (base class TEST3)
//48    | | {vfptr}
//52    | | {vbptr}
//56    | | a3
//  | +---
//60    | a5
//  +---

  如果TEST4和TEST5未保证对TEST3的虚继承,TEST6就会存在两个TEST3,不过TEST依旧只存在一个。
  不过这种情况下,TEST4对TEST2是否为虚继承就无关紧要了

3.结论

  2对1层的双虚继承,就足够保证消除二义性,如果保证TEST4这一层未来不会继续被继承,可以不用保证3对2层的双虚继承,这样能省下两个虚指针的空间;如果有后续的继承,那么要根据需要(是否要消除歧义、继承图的样子等),来选择是否要虚继承。

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

推荐阅读更多精彩内容