2. 结构体对齐规则详解

好,咱们直接进入正题。结构体内存对齐,说白了就是编译器在给结构体成员分配内存时,不是简单地挨个排排坐,而是会按照一套规则来「对齐」。这套规则,我把它拆成四个核心部分来讲。

2.1 默认对齐值:编译器说了算

每个编译器都有一个默认的对齐值。在大多数 32 位和 64 位系统上,这个值是 4 字节 或 8 字节。你可以用 #pragma pack(n) 来显式指定,但如果不指定,编译器就按自己的默认值来。

我个人习惯在跨平台项目里,明确用 #pragma pack(4) 或 #pragma pack(8) 来统一行为,避免不同编译器搞出幺蛾子。

核心原则: 每个成员的起始地址,必须是「自身大小」和「默认对齐值」中较小那个的整数倍。

2.2 成员对齐:每个成员都有自己的「对齐要求」

每个成员变量,都有它自己的对齐要求。比如:

  • char 类型:对齐要求是 1 字节(任何地址都行)
  • short 类型:对齐要求是 2 字节(地址必须是 2 的倍数)
  • int 类型:对齐要求是 4 字节(地址必须是 4 的倍数)
  • double 类型:对齐要求是 8 字节(地址必须是 8 的倍数)

编译器在安排成员时,会检查当前偏移量是否满足该成员的对齐要求。如果不满足,就会在它前面插入一些「填充字节」,直到满足为止。

避坑指南: 我曾经在嵌入式项目里,把一个 int 放在 char 后面,结果发现结构体大小比预期多了 3 个字节。原因就是编译器在 char 后面插了 3 个填充字节,让 int 对齐到 4 字节边界。

2.3 结构体整体对齐:结构体本身也要对齐

结构体作为一个整体,它的大小必须是其「最宽成员」对齐要求的整数倍。为什么?因为如果你定义一个结构体数组,每个结构体元素的起始地址都必须满足最宽成员的对齐要求。

举个例子:

struct Example {
    char a;      // 1 字节
    int b;       // 4 字节
    short c;     // 2 字节
};

最宽成员是 int(4 字节),所以结构体整体大小必须是 4 的倍数。实际布局是:

  • a 占 1 字节,偏移 0
  • 填充 3 字节(偏移 1-3)
  • b 占 4 字节,偏移 4
  • c 占 2 字节,偏移 8
  • 填充 2 字节(偏移 10-11),让总大小达到 12(4 的倍数)

嗯,这里要注意:最后那 2 个填充字节,就是为了满足结构体整体对齐而加的。

2.4 填充字节:看不见的「内存浪费」

填充字节是编译器自动插入的,目的是让成员对齐。它们不存储任何有效数据,纯粹是「占位符」。但如果你不注意,它们会悄悄增加结构体的大小。

你想想看,一个结构体里如果成员顺序排得不好,填充字节可能会占掉很大比例。我见过一个项目,结构体里全是 char 和 int 交替排列,结果填充字节占了将近一半的空间。

警告: 填充字节的内容是未定义的!不要试图读取它们。不同编译器、不同优化级别下,填充字节的值可能完全不同。我曾经在调试时看到填充字节里出现了「0xCC」,那是 MSVC 的调试模式填充,换到 GCC 就变成了 0x00。

2.5 知识体系结构图

下面这张图,帮你理清结构体对齐的整个逻辑链条:

结构体内存对齐规则 默认对齐值 #pragma pack(n) 成员对齐 自身大小 vs 默认值 填充字节 自动插入的占位 整体对齐 最宽成员倍数 编译器默认值 32位: 4字节 64位: 8字节 可被 #pragma 覆盖 起始地址 = min(自身, 默认) 的倍数 char: 1字节 short: 2字节 int: 4字节 | double: 8字节 插入在成员之间 不存储有效数据 内容未定义 不可读取 结构体大小 = 最宽成员倍数 影响数组布局 末尾可能填充 保证数组元素对齐 最终结果:结构体大小 = 所有成员对齐后的总大小 + 末尾填充 优化目标:减少填充字节,节省内存 示例:struct { char a; int b; short c; } → 大小 = 12 字节

2.6 实战中的对齐计算

咱们来手算一个结构体的大小,加深理解:

struct Data {
    char    flag;    // 1 字节,偏移 0
    double  value;   // 8 字节,需要偏移 8(填充 7 字节)
    int     count;   // 4 字节,偏移 16(已对齐)
    short   id;      // 2 字节,偏移 20
    // 末尾填充 6 字节,让总大小达到 24(8 的倍数)
};

计算过程:

  1. flag 占 1 字节,偏移 0
  2. 为了对齐 value(8 字节),填充 7 字节,偏移变成 8
  3. value 占 8 字节,偏移 8-15
  4. count 占 4 字节,偏移 16-19(已对齐到 4)
  5. id 占 2 字节,偏移 20-21
  6. 最宽成员是 double(8 字节),总大小必须是 8 的倍数。当前是 22,所以末尾填充 2 字节,变成 24

你看,这个结构体实际有效数据只有 15 字节,但占用了 24 字节,填充字节占了 9 个!

优化建议: 把成员按大小降序排列,可以显著减少填充。比如把 double 放最前面,int 其次,short 和 char 放最后。这样填充字节会少很多。