6月4日 13:41

C语言结构体内存对齐怎么算?三条规则和逐字节推演

结构体内存对齐是 C 语言面试的经典问题,也是实际项目中踩坑率很高的话题——sizeof 打出来的结果比预想的大,序列化/反序列化时数据错位,跨平台通信时结构体大小不一致,根源都在对齐和填充。

为什么需要对齐

CPU 访问内存不是逐字节的,而是按字长(32 位系统 4 字节,64 位系统 8 字节)一次读取。如果一个 int 跨了两次读取的边界,CPU 要读两次再拼接,性能下降。某些架构(如 ARM、SPARC)直接抛硬件异常。

所以编译器在结构体成员之间插入填充字节,让每个成员的起始地址落在自己"自然边界"上——这就是对齐。

三条对齐规则

  1. 成员对齐:每个成员的偏移量必须是该成员大小的整数倍。不够就补填充字节
  2. 结构体总大小:必须是最大成员大小的整数倍。不够就在末尾补填充字节
  3. 嵌套结构体:嵌套的结构体对齐到其自身最大成员大小的整数倍

逐字节推演

c
struct A { char c; // 偏移 0,占 1 字节 // 偏移 1-3:3 字节填充(因为 int 要对齐到 4 的倍数) int i; // 偏移 4,占 4 字节 }; // 总大小:8 字节(1 + 3填充 + 4,已是 4 的倍数,末尾不需补)

换一个成员顺序,浪费更多:

c
struct B { char c1; // 偏移 0,占 1 字节 // 偏移 1:1 字节填充(short 要对齐到 2 的倍数) short s; // 偏移 2,占 2 字节 char c2; // 偏移 4,占 1 字节 // 偏移 5-7:3 字节填充(总大小必须是最大成员 short 的 2 字节的倍数?不对——最大成员是 short 大小 2,所以总大小要是 2 的倍数。5+1=6,但 int 没出现,最大对齐数是 2,6 是 2 的倍数,所以总大小 6?不,实际要看最大成员。这里最大成员 short 是 2 字节,所以结构体对齐到 2。6 已是 2 的倍数,总大小 6。) }; // 实际总大小:6 字节

再来看一个浪费严重的例子:

c
struct Bad { char c1; // 偏移 0,1 字节 // 偏移 1-7:7 字节填充 double d; // 偏移 8,8 字节 char c2; // 偏移 16,1 字节 // 偏移 17-23:7 字节填充(总大小须为 8 的倍数) }; // 总大小:24 字节——3 个成员实际只用了 10 字节,浪费了 14 字节

调整成员顺序,把大的放前面:

c
struct Good { double d; // 偏移 0,8 字节 char c1; // 偏移 8,1 字节 char c2; // 偏移 9,1 字节 // 偏移 10-15:6 字节填充(总大小须为 8 的倍数) }; // 总大小:16 字节——比 Bad 少了 8 字节

优化方法

成员按大小降序排列

最简单有效的手段:把 double 放前面,int 次之,short 再次,char 最后。填充字节最少。大型项目(如 Linux 内核)有专门的脚本检查结构体填充浪费。

#pragma pack 紧凑对齐

网络协议头、文件格式头等场景需要精确控制布局,可以用编译器指令取消对齐:

c
#pragma pack(push, 1) // 保存当前对齐设置,设为 1 字节对齐 struct PacketHeader { char type; int length; short flags; }; #pragma pack(pop) // 恢复之前的对齐设置

1 字节对齐下没有填充,sizeof(struct PacketHeader) = 7。但访问 length 可能不对齐,某些架构上性能下降甚至崩溃。所以 #pragma pack 只用于和外部协议对接的场合,不要在内部数据结构上滥用。

位域

用位域把多个小字段压缩到一个基本类型里:

c
struct Flags { unsigned int ready : 1; unsigned int error : 1; unsigned int mode : 6; // 总共 8 位,1 字节就够 };

注意:位域的内存布局依赖编译器实现,跨编译器/跨平台不保证一致。不同编译器分配位域的方向可能不同,用于网络通信时要格外小心。

跨平台注意事项

  • 对齐数不同:32 位和 64 位系统上,double 的对齐要求可能不同(4 vs 8),同一个结构体大小可能不一样
  • 指针大小不同:32 位指针 4 字节,64 位 8 字节,含指针的结构体大小不同
  • 字节序不同:大端/小端影响多字节成员的存储顺序,和填充无关但和序列化相关
  • 网络传输:不要直接 send/recv 结构体——填充、对齐、字节序都可能不一致。正确做法是逐字段序列化,或用 #pragma pack(1) 的专用协议结构体
标签:C语言