共计 2240 个字符,预计需要花费 6 分钟才能阅读完成。
为什么要字节对齐和填充
结构体中的变量在内存中的地址在大多数情况下并不会是连续的,而是在变量首地址中间有一些空余。这是编译器为了提高对数据访问的效率而刻意为之的。
这个问题可以被分解成两个子问题:
- 为什么要字节对齐?
- 为什么结构体要在变量首地址之间“填充“空隙?
首先来回答第一个问题,CPU 从内存中取数据的时候是以 4 字节一组来取的(不同的硬件可能有差异,也可能是 8 字节,为了简单这里就按 4 字节解释),假设有一个变量存在地址为 1 的地址,那么 CPU 拿到地址为 0 的 4 个字节后还需要把地址 0 存放的数据扔掉,这样一来就增加了使用的 CPU 周期,当然也就比较低效了。
至于第二个问题,这还跟缓存机制有关,正确填充的结构体有利于减少 cache miss 和 cache flush、降低总线传输时间、降低数据竞争风险等。
总之,如果一个结构体没有正确对齐和填充,对它的访问和操作会变得比较低效。所以编译器会填充空隙,以空间来换取时间。当然,如果就是要省空间,那也有对应的办法,这个后边会介绍。另外,这也是在嵌入式编程中通常禁止或者不推荐使用 malloc 函数的原因之一,因为大多数嵌入式 MCU 并没有内存管理单元。由于字节对齐的现象,很容易产生内存碎片,从而导致可利用的内存越来越少,最后系统崩溃。
结构体到底在内存中如何存放
开门见山,其存放规则如下:
- 结构体成员的首地址要是其所占空间的整数倍
- 结构体的总体大小要是占用空间最大成员所占地址的整数倍
- 结构体的首地址要是占用空间最大成员所占地址的整数倍
下面对这几条规则做个解释,第一条很好理解,字节对齐。比如一个 int
变量在 32 位系统上占 4 字节,double
占 8 字节,那么它们的存放地址就要是 4 / 8 的整倍数。第二条,主要计算完最后一个成员地址后,要看计算的大小是不是占用地址最大的成员所占空间的整倍数。例如:
struct Example1 {
char a;
int b;
double c;
};
计算步骤如下:
char a
占 1 个字节,首地址 0,0%1==0
不需要填充,已分配大小:1 字节int b
占 4 个字节,首地址 1,1%4!=0
需要填充 3,已分配大小:8 字节double c
占 8 个字节,首地址 8,8%8==0
不需要填充,已分配大小:16 字节- 最后一个成员计算完毕,已分配大小 16 字节,是最大成员(
double
)的整倍数,不用填充,总大小:16 字节
另外需要注意的是,成员声明的顺序也会影响最后的大小:
struct Example2 {
char a;
int b;
char c;
double d;
};
struct Example3 {
char a;
char b;
int c;
double d;
};
Example2
和 Example3
分别占 24 字节和 16 字节,这里给出 Example3
的计算过程。
Example3
:
char a
占 1 个字节,首地址 0,0%1==0
不需要填充,已分配大小:1 字节char b
占 1 个字节,首地址 1,1%1==0
不需要填充,已分配大小:2 字节int c
占 4 个字节,首地址 2,2%4!=0
需要填充 2,已分配大小:8 字节double d
占 8 个字节,首地址 8,8%8==0
不需要填充,已分配大小:16 字节- 最后一个成员计算完毕,已分配大小 16 字节,是最大成员(
double
)的整倍数,不用填充,总大小:16 字节
再来看一个稍复杂的例子:
struct Example6 {
int a;
char * b;
short c;
char d[2];
short e[4];
}sE6;
其中 char *b
是指向字符类型的指针,在 32 位系统上占 4 个字节。而计算数组成员的起始地址时要按照数组存放的类型来定,而不是数组所占的整体大小,比如这里在计算完 d 后的大小是 12 字节,这里并不用填充至 8 的倍数(e 这个数组占连续的 8 字节)。来使用 VS 来验证下:
sE6.a = 0xaaaaaaaa;
sE6.b = (char*)0xbbbbbbbb;
sE6.c = 0xcccc;
sE6.d[0] = 0xd0;
sE6.d[1] = 0xd1;
sE6.e[0] = 0xe0e0;
sE6.e[3] = 0xe3e3;
关于开头提到的存放规则的第三点,结构体的首地址要是占用空间最大成员所占地址的整数倍
。这个当结构体存在嵌套时会用到。计算时候不能把先嵌套的结构体当成一个成员变量计算,而是要使用第三条规则。
typedef Example2 sE2;
struct Example4 {
char a;
sE2 b;
int c;
double d;
};
Example4
:
char a
占 1 个字节,首地址 0,0%1==0
不需要填充,已分配大小:1 字节struct Example2
占 24 字节,首地址 1,struct Example2
的最大变量占 8 字节,1%8!=0
需要填充 7 字节(而不是 23 字节),已分配大小:32 字节int b
占 4 个字节,首地址 32,32%4==0
不需要填充,已分配大小:36 字节double c
占 8 个字节,首地址 36,36%8!=0
需要填充 4 字节,已分配大小:48 字节- 最后一个成员计算完毕,已分配大小 48 字节,是最大成员(
sizeof(double)=8
而 不是sizeof(struct Example2)=24
)的整倍数,无需填充,总大小:48 字节
宏 字节对齐
你可以使用 #pragma pack()
来自定义字节对齐数。括号内填入你期望的数字,不填即为默认方式。
例如当你使用了 #pragma pack(1)
时,此时完全没有填充和对齐,Example4
占用空间为 28 字节。
当使用 #pragma pack(4)
时,Example4
占用空间为 40 字节。