为什么要字节对齐和填充
结构体中的变量在内存中的地址在大多数情况下并不会是连续的,而是在变量首地址中间有一些空余。这是编译器为了提高对数据访问的效率而刻意为之的。
这个问题可以被分解成两个子问题:
- 为什么要字节对齐?
- 为什么结构体要在变量首地址之间“填充“空隙?
首先来回答第一个问题,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字节。
博主讲的很好,速更,夜不能寐