2. 结构体对齐规则详解
好,咱们直接进入正题。结构体内存对齐,说白了就是编译器在给结构体成员分配内存时,不是简单地挨个排排坐,而是会按照一套规则来「对齐」。这套规则,我把它拆成四个核心部分来讲。
2.1 默认对齐值:编译器说了算
每个编译器都有一个默认的对齐值。在大多数 32 位和 64 位系统上,这个值是 4 字节 或 8 字节。你可以用 #pragma pack(n) 来显式指定,但如果不指定,编译器就按自己的默认值来。
我个人习惯在跨平台项目里,明确用 #pragma pack(4) 或 #pragma pack(8) 来统一行为,避免不同编译器搞出幺蛾子。
核心原则: 每个成员的起始地址,必须是「自身大小」和「默认对齐值」中较小那个的整数倍。
2.2 成员对齐:每个成员都有自己的「对齐要求」
每个成员变量,都有它自己的对齐要求。比如:
char类型:对齐要求是 1 字节(任何地址都行)short类型:对齐要求是 2 字节(地址必须是 2 的倍数)int类型:对齐要求是 4 字节(地址必须是 4 的倍数)double类型:对齐要求是 8 字节(地址必须是 8 的倍数)
编译器在安排成员时,会检查当前偏移量是否满足该成员的对齐要求。如果不满足,就会在它前面插入一些「填充字节」,直到满足为止。
避坑指南: 我曾经在嵌入式项目里,把一个 int 放在 char 后面,结果发现结构体大小比预期多了 3 个字节。原因就是编译器在 char 后面插了 3 个填充字节,让 int 对齐到 4 字节边界。
2.3 结构体整体对齐:结构体本身也要对齐
结构体作为一个整体,它的大小必须是其「最宽成员」对齐要求的整数倍。为什么?因为如果你定义一个结构体数组,每个结构体元素的起始地址都必须满足最宽成员的对齐要求。
举个例子:
struct Example {
char a; // 1 字节
int b; // 4 字节
short c; // 2 字节
};
最宽成员是 int(4 字节),所以结构体整体大小必须是 4 的倍数。实际布局是:
a占 1 字节,偏移 0- 填充 3 字节(偏移 1-3)
b占 4 字节,偏移 4c占 2 字节,偏移 8- 填充 2 字节(偏移 10-11),让总大小达到 12(4 的倍数)
嗯,这里要注意:最后那 2 个填充字节,就是为了满足结构体整体对齐而加的。
2.4 填充字节:看不见的「内存浪费」
填充字节是编译器自动插入的,目的是让成员对齐。它们不存储任何有效数据,纯粹是「占位符」。但如果你不注意,它们会悄悄增加结构体的大小。
你想想看,一个结构体里如果成员顺序排得不好,填充字节可能会占掉很大比例。我见过一个项目,结构体里全是 char 和 int 交替排列,结果填充字节占了将近一半的空间。
警告: 填充字节的内容是未定义的!不要试图读取它们。不同编译器、不同优化级别下,填充字节的值可能完全不同。我曾经在调试时看到填充字节里出现了「0xCC」,那是 MSVC 的调试模式填充,换到 GCC 就变成了 0x00。
2.5 知识体系结构图
下面这张图,帮你理清结构体对齐的整个逻辑链条:
2.6 实战中的对齐计算
咱们来手算一个结构体的大小,加深理解:
struct Data {
char flag; // 1 字节,偏移 0
double value; // 8 字节,需要偏移 8(填充 7 字节)
int count; // 4 字节,偏移 16(已对齐)
short id; // 2 字节,偏移 20
// 末尾填充 6 字节,让总大小达到 24(8 的倍数)
};
计算过程:
flag占 1 字节,偏移 0- 为了对齐
value(8 字节),填充 7 字节,偏移变成 8 value占 8 字节,偏移 8-15count占 4 字节,偏移 16-19(已对齐到 4)id占 2 字节,偏移 20-21- 最宽成员是
double(8 字节),总大小必须是 8 的倍数。当前是 22,所以末尾填充 2 字节,变成 24
你看,这个结构体实际有效数据只有 15 字节,但占用了 24 字节,填充字节占了 9 个!
优化建议: 把成员按大小降序排列,可以显著减少填充。比如把 double 放最前面,int 其次,short 和 char 放最后。这样填充字节会少很多。