2. 结构体对齐规则:默认对齐规则、成员顺序对大小的影响、嵌套结构体的对齐

结构体对齐,说白了就是编译器在给结构体成员分配内存时,不是简单地挨个排排坐。它会根据每个成员的类型,在内存地址上做一些「对齐」处理。为什么要这么干?因为CPU读取内存时,从对齐的地址读最快。你想想看,如果CPU每次读4字节,结果你的int放在了一个奇数地址上,那它就得读两次再拼起来——效率直接打对折。

我刚开始学C语言时,总觉得对齐是编译器多管闲事。直到有一次在嵌入式项目里,我手动算结构体大小,算出来是10字节,结果sizeof告诉我12字节。当时我就懵了。嗯,从那以后,我再也不敢忽略对齐规则了。

2.1 默认对齐规则

默认对齐规则其实就三条,记牢了就行:

  • 规则一:每个成员的对齐边界 = 自身大小。比如char是1字节对齐,short是2字节对齐,int是4字节对齐,double是8字节对齐。
  • 规则二:结构体的总大小,必须是最大成员对齐边界的整数倍。如果最大成员是double(8字节),那结构体总大小必须是8的倍数。
  • 规则三:成员按声明顺序依次存放,但会在成员之间插入填充字节(padding),以保证每个成员都落在自己的对齐边界上。

举个例子,你看这个结构体:

struct S1 {
    char a;   // 1字节
    int  b;   // 4字节
    char c;   // 1字节
};

你猜sizeof(S1)是多少?直觉上1+4+1=6字节,对吧?但实际是12字节。为什么会这样?

我来给你拆解一下:

  • a占1字节,地址0
  • b需要4字节对齐,所以从地址4开始,地址1~3被填充了
  • c占1字节,地址8
  • 结构体总大小必须是最大成员(int,4字节)的倍数,所以填充到12字节

你看,白白浪费了6个字节。我在项目中遇到过类似的情况,一个结构体里就几个字段,结果sizeof出来比预期大了一倍。当时排查了半天,才发现是成员顺序没排好。

核心结论:默认对齐规则下,编译器会在成员之间和结构体末尾插入填充字节,以保证每个成员都落在自然对齐的地址上。

2.2 成员顺序对大小的影响

这一点很有意思——同样的成员,换个顺序,结构体大小可能就变了。说白了,就是通过调整顺序来减少填充字节。

还是刚才那个例子,我们把成员顺序换一下:

struct S2 {
    int  b;   // 4字节
    char a;   // 1字节
    char c;   // 1字节
};

现在sizeof(S2)是多少?答案是8字节。你看:

  • b占4字节,地址0~3
  • a占1字节,地址4
  • c占1字节,地址5
  • 结构体末尾填充到8字节(4的倍数)

从12字节降到8字节,省了33%的空间。我建议你在写结构体时,养成一个习惯:把大的成员放在前面,小的成员放在后面。这样能最大程度减少填充。

个人经验:我在做网络协议解析时,经常需要定义报文头结构体。如果成员顺序没排好,不仅浪费内存,还会导致解析出来的字段值不对——因为填充字节把数据位置给挤偏了。所以每次定义结构体前,我都会先在纸上画一下内存布局。

再给你看一个更极端的例子:

struct S3 {
    char a;     // 1字节
    double b;   // 8字节
    char c;     // 1字节
};

struct S4 {
    double b;   // 8字节
    char a;     // 1字节
    char c;     // 1字节
};

sizeof(S3) = 24字节,sizeof(S4) = 16字节。同样的成员,只是顺序不同,差了8个字节。你想想看,如果结构体数组有1000个元素,那就是8KB的差距。在嵌入式系统里,这可不是小数目。

2.3 嵌套结构体的对齐

嵌套结构体的对齐规则,说白了就是「递归应用对齐规则」。内层结构体作为一个整体,它的对齐边界取决于它内部最大的成员。

看这个例子:

struct Inner {
    char a;     // 1字节
    int  b;     // 4字节
};  // sizeof(Inner) = 8

struct Outer {
    char c;         // 1字节
    struct Inner d; // 8字节
    char e;         // 1字节
};

sizeof(Outer)是多少?我们来算一下:

  • c占1字节,地址0
  • Inner的对齐边界是4(因为内部有int),所以从地址4开始放d,地址1~3填充
  • d占8字节,地址4~11
  • e占1字节,地址12
  • 结构体总大小必须是最大成员对齐边界的整数倍。最大成员是谁?是Inner里的int(4字节),还是Outer里的成员?答案是取所有成员(包括嵌套结构体内部的成员)中最大的对齐边界。这里最大是4字节,所以总大小填充到16字节

所以sizeof(Outer) = 16字节。

注意:嵌套结构体本身作为一个成员,它的对齐边界不是它自己的大小,而是它内部最大成员的对齐边界。这一点很容易搞错。我曾经在写一个文件系统驱动时,就因为没搞清楚这个规则,导致结构体大小算错,读写文件时数据偏移全乱了。排查了整整一个下午才找到原因。

再给你看一个更复杂的嵌套例子:

struct Inner2 {
    char a;     // 1字节
    double b;   // 8字节
};  // sizeof(Inner2) = 16

struct Outer2 {
    struct Inner2 d; // 16字节,对齐边界8
    char e;          // 1字节
    int  f;          // 4字节
};

sizeof(Outer2) = 24字节。为什么?

  • d从地址0开始,占16字节(地址0~15)
  • e占1字节,地址16
  • f需要4字节对齐,所以从地址20开始,地址17~19填充
  • f占4字节,地址20~23
  • 最大对齐边界是8(Inner2里的double),24是8的倍数,正好

嵌套结构体对齐总结:

  • 内层结构体的对齐边界 = 它内部最大成员的对齐边界
  • 外层结构体的总对齐边界 = 所有成员(递归展开)中最大的对齐边界
  • 内层结构体作为一个整体,它的起始地址必须对齐到它自己的对齐边界

好了,关于结构体对齐的规则,我就讲到这里。说白了就是三件事:成员要对齐到自己的边界、结构体总大小要对齐到最大成员的倍数、嵌套时要递归考虑。记住这些,你在实际项目中就不会被sizeof坑到了。

结构体对齐规则知识体系 默认对齐规则 • 成员对齐边界 = 自身大小 • 总大小 = 最大成员倍数 • 成员间插入填充字节 示例: char + int + char = 12B 而非 1+4+1=6B 成员顺序影响 • 大成员在前,小成员在后 • 减少填充字节浪费 • 优化内存占用 对比: int+char+char = 8B char+int+char = 12B 节省33%空间! 嵌套结构体对齐 • 递归应用对齐规则 • 内层对齐边界 = 内部最大成员 • 外层对齐边界 = 递归最大 示例: Inner: char+int = 8B Outer: char+Inner+char = 16B 核心原则:对齐到边界 → 减少填充 → 递归处理嵌套

实用建议:如果你不确定结构体的大小,可以用offsetof宏来查看每个成员的偏移量。比如 offsetof(struct S1, b) 就能告诉你b成员在结构体中的偏移位置。我在调试时经常用这个宏来验证我的对齐计算是否正确。