0. 什么是内存对齐
现代计算机在处理数据时,按照某个“单位”来处理。32位机器,每次处理32位、4字节的二进制数据,64位同理。
内存对齐指的是计算机系统对基本数据类型合法地址做出了一些限制,要求某种类型对象的地址必须是某个值的倍数。
本文着重于内存对齐的基本原理,有关复杂情况下(虚函数、继承等)的内存对齐方案,请参考【深度C++】之“类内存布局”。
1. 为什么需要内存对齐
举例来说:
class MyClass {
int i;
char c;
double d;
};
若内存未对齐,如下图:
上图左侧数字表示内存地址,上方绿色数字表示相对于品红色数字的偏移字节;一个紫色的小格子表示一个字节。图示为32bit机器的存储结构。
每次处理my_class.d
时候,都要读出854620
位置的数据,裁减后段;读出854624
位置的数据,全部需要;读出854628
位置的数据,裁减出前段,然后拼接得到my_class.d
的值,无疑浪费了很多时间。
对齐后,如下图:
每次处理my_class.d
就简单多了,虽然浪费了空间,带来的却是效率的提升。
2. 内存对齐的原则
内存对齐有三个原则:
- 确定对齐单位k,k的单位是字节,取下面三个数值的最小值:
- 32位机器默认4字节,64位机器默认8字节;
- 预编译指令
#pragma pack(n)
中的n的值,n取1、2、4、8、16; - 结构体或类中长度最大成员;
- 除了第一个成员数据外,其余各成员数据距首地址偏移量为
min(k, p)
的整数倍,其中p为当前成员数据的字节数; - 结构体或类的整体大小,必须为k的整数倍。
举例MyClass
来说(假设int占4字节,char占1字节,double占8字节):
class MyClass {
int i;
char c;
double d;
};
- 我的电脑是64位机器,取8;预编译指令没有设置过,我的电脑是8;
MyClass
中最大的数据成员double类型,也是8;取三者最小,因此对齐单位:k = 8; - ①第一个成员
int i
在首位,不需要偏移,占4个字节;②第二个成员char c
占1个字节,放在min(8, 1) = 1的整数倍位置,因此可以直接放在int后面,偏移为4;③第三个成员double d
不可以直接放在c的后面,这样它的偏移是5。d
必须放在min(8, 8) = 8的整数倍的位置,可以取8,所以空出来3个字节。 - 排满之后,类的整体为16个字节,4(int) + 1(char) + 3(浪费) + 8(double) = 16,符合8的整数倍。
因此内存布局如下图:
再举一例:
class MyClass {
int i;
char c;
double d;
char p;
};
前三个成员都一样的,主要是最后一个成员,要遵循第三个原则。
当把char p
添加到原来的double d
后面时,可以直接紧接着double d
,偏移为16;这样算出来的类的大小为16 + 1(char) = 17,不符合第三条原则,因此需要填充到24(8的3倍),即添加7个浪费的字节,4(int) + 1(char) + 3(浪费) + 8(double) + 1(char) + 7(浪费) = 24。
最终结果如下图:
3. 总结
C++中的内存对齐,通过牺牲空间的方式,换来了执行效率的提升。它通过三个原则,按照类或结构体中的声明顺序,逐步确认每个成员的位置,最终实现了成员数据的内存对齐。
本文只讨论了最简单的对齐情况,对于复杂的结构体和类,要考虑静态成员、虚函数、继承等情况,详细内容请参考【深陷C++】之“类内存布局”。