前言
本文会展示内存对齐,及继承、虚继承等各个情况下内存的布局,并根据结果总结使用场景。
基本调试方法
使用编译器自带的工具,在Visual Studio下,右键解决方案,在弹出的菜单下,点击属性:
/d1 reportAllClassLayout
,点击确定。/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.h和immintrin.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层的双虚继承,这样能省下两个虚指针的空间;如果有后续的继承,那么要根据需要(是否要消除歧义、继承图的样子等),来选择是否要虚继承。