1.为什么会出现内存对齐?
内存对齐的主要目的是为了提高CPU对内存的访问效率,CPU访问内存数据时会受到地址总线宽度的限制,也就是说CPU一次能从内存中取多少数据。例如Intel 64位CPU,每个总线周期都是从偶数地址开始读取64位的内存数据,如果数据存放的地址不是从偶数开始,那么就可能会出现需要两个总线周期才能读到想要的数据。因此,在内存中存放数据时为了能让CPU一次读取到数据,从而提升性能需要进行内存对齐处理。下面假设CPU地址总线为64位(占8字节),考虑当一个int类型(占4字节)数据存储到地址为0x06位置时,CPU该如何读取内存中的这个int类型数据。
(1).先读取0x00~0x08这8个字节,由于int类型的数据开始存储的地址是0x06。因此,它的前两个字节会存储在地址为0x00~0x08的后两个字节中;
(2).后读取0x08~0x0F这8个字节,由于int类型的数据占4个字节。因此,它的后两个字节会存储在地址为0x08~0x0F的前两个字节中;
2.内存对齐基本单位
通过上面的分析,我们已经知道内存对齐的原因是为让CPU一次能读取内存中的数据,从而提升性能。因为CPU只能使用基本数据类型(char、int、float、double等),于是像数组、结构体、枚举等复合数据类型CPU都不能使用。所以,内存对齐的基本单位是基本数据类型,目的是为了让CPU能一次性获取基本类型的值。
3.内存对齐规则【重点】
思考下面的示例代码,结构体A和结构体B中的成员类型完全一样,但它们分别占用的内存空间大小却不一样。
#include <iostream> using namespace std; struct A{ char a; int b; short c; }; struct B{ short c; char a; int b; }; int main(){ cout << "sizeof(A) = "<< sizeof(A) << endl; // 8 cout << "sizeof(B) = " << sizeof(B) << endl; // 12 return 0; }
为解释上面代码运行的不同结果,下面直接总结出内存对齐的三大规则:
(1).对于结构体中的每个成员:第一个成员的偏移量默认是0,排在它后面的成员,其当前偏移量必须是当前成员类型的整数倍。
(2).结构体中所有成员各自内存对齐后,结构体本身还要进行一次内存对齐,为了保证整个结构体占用内存的大小是结构体中最大数据成员的最小整数倍。
(3).如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型和结构体内最大数据成员的类型。
下面具体分析上面代码的运行结果:首先,由于结构体A的第一个成员a是char类型,占1个字节且偏移量默认为0;第二个成员b是int类型,占4个字节。根据上述规则一,成员b的偏移量必须是int类型的整数倍。因此,编译器会在a成员后面额外增加3个字节缓冲区,从而保证b的偏移量是4字节刚好是b成员基本数据类型的整数倍(1倍);第3个成员c是short类型,此时c的偏移量恰好是8字节,已经是short类型的整数倍,因此成员b和成员c之间不用额外增加缓冲字节。
但是,结构体A的大小为8+2=10字节。根据规则二,结构体A的大小必须是其最大成员类型(int)的整数倍,所以在10字节基础上再增加2个字节。于是,最终结构体的大小为12字节,满足规则二。
结构体成员 偏移量 成员自身原本占用空间 char a 0 1 缓冲补齐 1 3(规则一) int b 4 4 short c 8 2 缓冲补齐 10 2(规则一)
类似地,对结构体B的成员进行分析后,得到如下结果:
结构体成员 偏移量 成员自身原本占用空间 short c 0 2 char a 2 1 缓冲补齐 3 1(规则一) int b 4 4
4.难度升级
下面考虑一个复杂的示例代码,结构体中含联合体、枚举等复合类型数据成员。如下所示:
#include <iostream> using namespace std; struct BU { int number; // 占4字节 union UBuffer { char buffer[13]; // 该成员占13字节,因此需要填充3字节,该成员实际占用16字节空间大小 int number; }ubuf; int a; // 占4字节,当前偏移量为20字节,恰好为4字节的整数倍,不用填充 double d; // 占8字节,当前偏移量为28字节,但不符合规则二,最终结果需填充4字节共32字节。 }bu; int main(){ cout << "sizeof(BU) = "<< sizeof(BU) << endl; // 32 return 0; }
对上面的结构体BU稍微调整一下成员a和成员d的顺序,结果就会大不一样:
#include <iostream> using namespace std; struct BU { int number; // 占4字节 union UBuffer { char buffer[13]; // 该成员占13字节,因此需要填充3字节,该成员实际占用16字节空间大小 int number; // 占4字节,当前偏移量20字节 }ubuf; double d; // 占8字节,根据规则一,当前偏移量填充后为24字节 int a; // 占4字节,当前偏移量为32字节 }bu; // 结构体BU最终的偏移量为36字节,不满足规则二,填充4字节后为40字节 int main(){ cout << "sizeof(BU) = "<< sizeof(BU) << endl; // 40 return 0; }
对结构体中包含union类型的成员有疑问的话,可以再分析下面的示例代码:
#include<iostream> using namespace std; struct BD { short number; union UBuffer { char buffer[13]; int number; }ubuf; }bu; int main(){ cout << "sizeof(BU) = "<< sizeof(BD) << endl; // 20 return 0; }
上述代码段运行的结果sizeof(BD)=0 + 2 + 2(填充2字节) + 13 + 3(填充3字节)=20字节。为啥最终结果不是: sizeof(BD)=0 + 2 + 13 + 1(填充1字节) =16字节呢?这是由于union类型比较特别,计算union成员的偏移量时,需要根据union内部最大成员类型进行填充补齐。因此,为了保证偏移量是union中最大成员int类型的整数倍,需要在short类型成员number后面填充2字节。
5.解析规则三
将编写代码时,最开始位置加上#pragma pack(1)时,以1个字节进行对齐时,属于最简单的情况。结构体的大小直接等于所有成员的类型大小之和,此时与成员排列顺序无关。一般来说,奇数个字节对齐没有意义,正常情况下程序员并不关心编译器对内存对齐所进行的优化操作。
C语言中的offsetof()函数可以用来查看特定的结构体成员在结构体中的偏移量,写代码时可以用它来检验上面的分析结果。函数原型如下所示:
#define offsetof(type, member) (size_t)&(((type *)0)->member)
基本原理:强制将结构体(类型为type)的起始地址置为0,然后输出其成员的地址,该地址的大小就是成员在结构体中的偏移量。具体使用见下例所示:
#include <stdio.h> #define offset(type, member) (size_t)&(((type *)0)->member) #define STRUCT_E_ADDR(s,e) printf("%5s size = %2d %16s addr: %p\n", #s, sizeof(s), #s"."#e, &s.e) #define STRUCT_E_OFFSET(s,e) printf("%5s size = %2d %16s offset: %2d\n", #s, sizeof(s), #s"."#e, offset(__typeof__(s),e)) #define STRUCT_E_ADDR_OFFSET(s,e) printf("%5s size = %2d %16s addr: %p, offset: %2d\n", #s, sizeof(s), #s"."#e, &s.e, offset(__typeof__(s),e)) typedef struct { int e_int; char e_char; }S1; typedef struct { int e_int; double e_double; }S11; int main() { S1 s1; STRUCT_E_ADDR_OFFSET(s1, e_int); STRUCT_E_ADDR_OFFSET(s1, e_char); S11 s11; STRUCT_E_ADDR_OFFSET(s11, e_int); STRUCT_E_ADDR_OFFSET(s11, e_double); return 0; }