Dev tool

结构体内存对齐

三个成员一共 6 字节,sizeof 却是 12。把成员逐个摆进内存, 看清楚哪几个字节是填充、为什么省不掉,以及换个顺序能省多少。

目标平台

类型按 ABI 取标准的 size / align;数组长度只影响占用字节,不影响对齐。 真实工程里请以 sizeof 和 offsetof 的输出为准。

sizeof — —
成员占用 — —
填充字节 — —
结构体对齐 — —
内存布局 —
# 类型 偏移 大小 对齐 前置填充
按对齐重排 —
对应的 C 定义

三条规则就够了

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 才会出现空洞。