三条规则就够了
C 标准只说了「成员按声明顺序存放」,具体怎么填空隙由 ABI 规定。实际算的时候只要记住三条:
- 每个成员的偏移必须是它对齐值的整数倍,不是就在前面补字节
- 结构体总大小必须是最大对齐值的整数倍,不是就在尾部补字节(为了让数组里每个元素的成员都对齐)
#pragma pack(n)把每个成员的对齐上限压到 n,通常是给协议报文用的
第三条规则常被忽略,但它解释了「明明成员排在 12,sizeof 却是 16」: 结构体后面可能跟着另一个同类型变量,尾部不对齐,下一个变量的成员就错位了。
填充不是浪费,是给硬件让路
一个 4 字节的 int 如果落在地址 1 上,跨了两个 4 字节字;有的 CPU 需要两条指令拼出来, 有的直接抛异常。对齐就是把访问压回一次总线事务。
反过来说,嵌入式里省 RAM 有两条路:把成员按对齐降序排(本文工具的下半部分),
或者用 pack(1) 换紧凑布局——后者在严格对齐的核上会付出访问代价,
通信协议解析这类「只做字节搬运」的场景才划算。
跨平台别传结构体裸内存
32 位和 64 位下 long 与指针宽度不同,同一份结构体定义编译出来大小就不同;
大小端还会把字段顺序翻过来。所以协议、文件格式、共享内存里一律按字节逐个序列化,
别 memcpy 整个结构体。
常见问题
为什么两个 char 夹一个 int,sizeof 就从 6 变成 12?
因为 int 的偏移必须是 4 的倍数。char 占了 0 号字节,int 只能从 4 开始,中间 1~3 号字节被跳过;int 占 4~7,第二个 char 在 8,而结构体整体大小要是最大对齐(4)的倍数,所以尾部再补 3 个字节到 12。这 6 个字节既不存数据也不报错,纯粹是给对齐让路。
把成员按对齐值从大到小排,为什么就省空间?
填充的根源是「小成员先占位,把后面的对齐边界顶歪了」。大对齐的成员先放,它们的偏移天然是整数倍,后面小成员只要按自己的对齐找位置,往往正好落在空隙里。对齐都是 2 的幂时,按对齐降序排列得到的填充是最少的——这是编译器不会替你做、但可以手工做的事。
#pragma pack(1) 能省空间,为什么不默认这么干?
pack 会取消对齐要求,成员可能跨缓存行、跨页边界,访问时要么变慢,要么在严格对齐的架构(老 ARM、部分 DSP)上直接触发异常;某些平台还需要编译器生成多次访问再拼接的代码。另外取成员地址再强转会导致未定义行为。协议解析这类必须紧凑的场景才用它。
32 位和 64 位下同一个结构体为什么大小不同?
因为 long 和指针的宽度变了。LP64(x86-64 / AArch64)下 long 和指针都是 8 字节、对齐 8;ILP32(ARM AAPCS、多数 32 位 MCU)下是 4 字节、对齐 4。所以含指针的结构体在两种 ABI 下大小不同,跨平台传输结构体裸内存是经典事故。
结构体大小和成员顺序有关,那数组呢?
数组元素之间没有填充,因为元素类型相同、大小必然是对齐的整数倍(C 保证 sizeof(T) 是 alignof(T) 的倍数)。填充只发生在「不同类型相邻」和「结构体结尾」。所以 int[3] 一定是 12 字节,而 char 后面跟 int 才会出现空洞。