内存

结构体内存对齐与填充详解

结构体的大小往往不等于各成员大小之和,这就是内存对齐在起作用。理解对齐规则对编写高效、可移植的 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> 中,是排查对齐问题的利器。