结构体的大小往往不等于各成员大小之和,这就是内存对齐在起作用。理解对齐规则对编写高效、可移植的 C 代码至关重要,尤其在嵌入式和网络协议解析场景中。
一、为什么需要对齐
CPU 访问内存时通常以字长(4 或 8 字节)为单位。若数据跨字边界,可能需要两次总线周期。对齐能让硬件一次取完数据,提升访问效率。
- 32 位平台默认按 4 字节对齐
- 64 位平台默认按 8 字节对齐
- 某些架构(如 ARM)访问未对齐地址会直接触发异常
二、对齐的三条规则
结构体成员的偏移地址必须是其自身大小的整数倍;结构体总大小必须是最大成员大小的整数倍。
/* 示例:观察填充字节 */ struct Example { char a; /* 1 字节,偏移 0 */ int b; /* 4 字节,需对齐到 4,偏移 4 */ short c; /* 2 字节,偏移 8 */ char d; /* 1 字节,偏移 10 */ }; /* 总大小需是 4 的倍数,填充至 12 */ printf("%zu\n", sizeof(struct Example)); /* 12 */
内存布局:a(1) + 填充(3) + b(4) + c(2) + d(1) + 填充(1) = 12 字节
三、调整成员顺序减小体积
将大成员靠前、小成员靠后,可有效减少填充:
/* 优化后:仅需 8 字节 */ struct Optimized { int b; /* 4 字节,偏移 0 */ short c; /* 2 字节,偏移 4 */ char a; /* 1 字节,偏移 6 */ char d; /* 1 字节,偏移 7 */ }; /* 总大小 8,无需尾部填充 */
四、#pragma pack 强制 packed
网络协议或文件头常要求 1 字节对齐,可用编译器指令关闭填充:
#pragma pack(push, 1) struct PacketHeader { uint16_t len; uint32_t seq; uint8_t type; }; #pragma pack(pop) /* sizeof(struct PacketHeader) == 7,无填充 */
注意:packed 结构体访问可能更慢,且某些平台不支持非对齐访问。仅在必要时使用。
五、位域节省空间
当多个标志位只需 0/1 时,可用位域压缩存储:
struct Flags { unsigned int is_active : 1; unsigned int is_admin : 1; unsigned int level : 4; /* 0~15 */ unsigned int reserved : 2; }; /* 总共 8 位,实际占 4 字节(unsigned int 大小) */
六、总结
- 对齐提升访问速度,但会浪费空间
- 合理排列成员顺序可减少填充
#pragma pack用于协议解析等固定布局场景- 位域适合布尔标志与小范围枚举值
- 用
offsetof()验证实际偏移,不要靠猜测
offsetof 宏定义在 <stddef.h> 中,是排查对齐问题的利器。