目录
一 、结构体产生内存对齐原因
结构体会发生内存对齐的原因主要有以下几个:
- 硬件层面:
现代计算机CPU访问内存时以字长(Word Size)为单位进行读取(例如32位系统为4字节,64位系统为8字节)。当数据未对齐时(比如一个4字节的int变量起始地址为0x0003),CPU需要执行两次内存访问:第一次读取0x0000-0x0003,第二次读取0x0004-0x0007,然后将两次读取的结果拼接出目标数据。这种操作会造成性能损失,而内存对齐技术正是通过减少内存访问次数来提升性能。
- 平台原因 (移植原因):
不同硬件平台对内存访问存在限制,某些平台只能在特定地址读取特定数据类型,否则会触发硬件异常。
二 、结构体内存对齐规则
- 结构体的第一个成员总是从结构体起始位置开始对齐,也就是对齐到偏移量为0的地址处。
- 其余成员变量需要按照对齐数的整数倍地址处进行对齐,对齐数取以下两者的较小值:
- 编译器默认的对齐值
- 该成员变量的自身大小
注:Linux中gcc没有默认对齐数,直接使用成员自身大小作为对齐数。
对齐数 = 该结构体成员变量自身的大小与编译器默认的一个对齐数的较小值。
- 结构体总大小按最大对齐数(包括嵌套结构体中的成员)进行对齐,最终大小为该对齐数的整数倍
- 嵌套结构体的第一个成员的偏移量要根据外层结构体的对齐规则来确定,嵌套结构体的整体大小要对齐到其自身最大对齐数的整数倍,并且外层结构体的整体大小要考虑嵌套结构体的最大对齐数。
嵌套结构体:
struct Q1 {
char c;
int i;
};
struct Q2 {
char c;
struct Q1 q;
};
在这个例子中,struct Q1
的最大对齐数是 4,struct Q2
的整体大小要考虑 struct Q1
的最大对齐数 4 进行对齐。
三、结构体内存对齐现象重现
定义两个结构体S1和S2,它们包含相同的成员变量,只使其排列位置不同。
struct S1 {
char c1;
int i;
char c2;
};
struct S2 {
char c1;
char c2;
int i;
};
int main() {
printf("%zu\n", sizeof(struct S1));
printf("%zu\n", sizeof(struct S2));
return 0;
}
在Visual Studio中执行上述代码时,测试结果显示S1结构体占用12字节内存空间,而S2结构体仅需8字节。这种内存占用差异是由结构体的内存对齐机制造成的。
接下来我将展示结构体内存对齐的过程:
- 首先 确认该结构体中每个成员的较小对齐数:
成员本身大小 默认对齐数 较小对齐数
1 8 1
1 8 1
4 8 4
- 确认各成员对齐数后,便在内存中开辟空间,假设从图中的 '0' 处开辟空间,
- c1 存放在 0 处位置(结构体的第一个成员总是从结构体起始位置开始对齐)
- c2 存放在地址为对齐数(此处为1)的整数倍的位置,则存放在地址 '1 '处。
- i 存放在地址为对齐数(此处为4)的整数倍的位置,向下遍历,找到地址为4的倍数的位置,此时存放在地址 '4' 位置上
-
最后确认结构体的总大小,根据结构体内存对齐规则可知,结构体总大小与成员中最大对齐数有关,回看c1,c2和 i 。取最大对齐数(该例子中取值为4),可以得出该结构体总大小为4的整数倍,恰好此时结构体总大小为8,正好是4的倍数,所以整个结构体大小就为8.
四、结构体设计建议
通过前面的例子可以看出,结构体的大小与结构体成员的排列顺序相关。为了避免空间的浪费,在设计结构体时,将相同类型的成员存放在一起,并且每个不同类型的集合可以按照类型所占空间大小从小到大依次排列。
struct N
{
char c1;
char c2;
short s1;
short s2;
int i1;
int i2;
double d1;
double d2;
}
五、修改默认对齐数
结构体在对⻬⽅式不合适的时候,我们可以⾃⼰更改默认对⻬数。
如果涉及修改编译器的默认对齐数,我们需要借助于以下预处理命令:
#pragma pack(n)
- 在括号内填入数字后,默认对齐数将会被改为对应数字。
- 恢复默认对齐数,括号内不填写数字即可。
#include <stdio.h>
#pragma pack(1) // 修改默认对齐数为 1
struct Packed {
char c;
int i;
};
#pragma pack() // 恢复默认对齐数
int main() {
printf("%zu\n", sizeof(struct Packed));
return 0;
}