7.1 结构体与内存对齐


7.1 结构体与内存对齐

本节摘要:结构体成员按声明顺序排布,但每个成员的偏移必须是"自身对齐值"的倍数,编译器因此在成员之间插入填充字节;结构体总大小还要补齐到最大成员对齐值的倍数。这两条规则解释了 sizeof 的所有"意外"。本节手推一个结构体的完整布局,用 offsetof 核对,再讲压缩技巧与跨平台注意。

先看一个"算不平"的 sizeof

#include <stdio.h> struct Bad { char a; /* 1 字节 */ int b; /* 4 字节 */ char c; /* 1 字节 */ }; struct Good { int b; char a; char c; }; int main(void) { printf("Bad %zu\n", sizeof(struct Bad)); /* 12 */ printf("Good %zu\n", sizeof(struct Good)); /* 8 */ return 0; }

成员一样、顺序不同,大小差 50%。答案藏在填充字节里。

对齐与填充的两条规则

规则一(成员对齐):每个成员的偏移量必须是"该成员对齐值"的倍数。对齐值通常等于成员大小(char 为 1、short 为 2、int 为 4、double 为 8、指针为 8)。不满足就在前面垫填充。

规则二(结构体整体对齐):结构体总大小必须是"最大成员对齐值"的倍数,不足在尾部补齐——保证数组里下一个元素也满足规则一。

逐字节排 struct Bad

偏移 0 : a (char, 1 字节) 偏移 1 : 填充 (3 字节) —— b 需要 4 的倍数偏移 偏移 4 : b (int, 4 字节) 偏移 8 : c (char, 1 字节) 偏移 9 : 填充 (3 字节) —— 总大小补齐到 4 的倍数 合计 12 字节,其中 6 字节是填充

struct Good 把大成员放前面:b 占 0-3,a 占 4,c 占 5,尾部补 2,共 8 字节——填充从 6 降到 2。排座位原则:大成员在前,小成员靠后聚拢

图 1 两种成员顺序的布局对照

图 1 两种成员顺序的布局对照

用 offsetof 核对布局

标准头里提供了验证工具:

#include <stdio.h> #include <stddef.h> int main(void) { printf("a 偏移 %zu\n", offsetof(struct Bad, a)); /* 0 */ printf("b 偏移 %zu\n", offsetof(struct Bad, b)); /* 4 */ printf("c 偏移 %zu\n", offsetof(struct Bad, c)); /* 8 */ return 0; }

offsetof 的实现思路本身就是宏加指针运算的绝妙演出:((size_t)&((T*)0)->member)——把空指针强转成结构体指针再取成员地址,地址值恰是偏移量(从不真的解引用,零地址只做标尺)。第 5 章"指针只是地址运算"的又一块拼图。

工程要点

何时在意填充:百万级元素的数组里每个省 4 字节就是 4MB;嵌入式设备内存按 KB 计较——排座位省下的都是真金白银。桌面小对象则不必过度优化,可读性优先。

绝不能直接把结构体内存当字节流发出去。填充字节内容不确定、各平台对齐规则不同、字节序不同——两台机器直接内存转储结构体通信,收到的是天书。正确做法是逐字段序列化成明确格式的字节流。同理,fwrite(&stu, sizeof stu, ...) 整块写结构体文件,读回方必须完全相同的编译器与平台——跨平台数据交换禁用。

强制紧凑布局:需要按精确布局对接硬件或协议时,可让编译器紧凑排布(各编译器提供 pack 指令,紧凑到 1 字节对齐)。代价是未对齐访问可能变慢或崩溃,x86 宽容、某些 ARM 严格。用前先确认目标平台。

结构体赋值与传参:结构体可以整体赋值(编译器生成逐块复制,与 memcpy 等价),这是数组做不到的(数组名退化)。传参时大结构体建议传指针——第 4 章调用约定讲过,传值整块复制,传指针 8 字节。

⚠️ 常见坑:以为把 char 挪到后面就万事大吉。若结构体里有 double(对齐 8),头尾都要按 8 补;修改成员后 sizeof 变了,别把 sizeof 的旧值写死在代码或磁盘格式里。

💡 关键直觉:声明结构体成员时想一句"从大到小排队,零碎的聚在队尾"。一行排序换一半填充,性价比极高的习惯。

本节要点回顾

  • 两条对齐规则:成员偏移是自身对齐值倍数(垫填充)、总大小是最大对齐值倍数(尾部补齐)。
  • 成员顺序影响大小:大成员在前可把填充压到最小。
  • offsetof 验证布局:空指针标尺技巧,宏与指针运算的结合。
  • 结构体内存不能当字节流跨机器传输:填充内容、对齐、字节序全不确定,必须逐字段序列化。
  • 整体赋值合法且方便,但跨函数传大结构体传指针更省。

下一节看结构体的两个近亲:共用体的内存共享,与枚举、typedef 的命名之道。

常见疑问

问:对齐值永远等于成员大小吗? 主流平台上基本如此,但标准允许更大(例如某些平台对 double 要求更强对齐)。需要精确控制时用 alignof 查询成员的对齐值,别靠猜。

问:把结构体改成紧凑布局就更好吗? 不一定。紧凑布局省内存但可能产生未对齐访问,在宽容的 x86 上变慢、在严格的架构上直接崩溃。省内存与保性能二选一时,先确认目标平台行为。

问:嵌套结构体的对齐怎么算? 把内层结构体的最大对齐值当作一个"虚拟成员"的对齐值参与规则一与规则二即可。层层套用两条规则,任何嵌套布局都能手推出来。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U