图 1:同样三个成员,换个顺序从 24 字节降到 16 字节。
先说明属性:这是一篇历史笔试真题复盘,题目原型出自一份校园招聘笔试卷,与任何公司的当前招聘无关。结构体对齐是 C/C++ 笔试的钉子户,题目形式固定:给几个不同类型的成员,问 sizeof 是多少。会算只是及格,能讲清三条规则、顺手给出重排优化才是满分答法。
先把三条对齐规则钉死。
以 GCC/Clang 在 64 位平台的默认对齐为例:
- 成员对齐:每个成员的偏移量必须是它自身对齐值的倍数。char 对齐 1、uint32_t 对齐 4、uint64_t 对齐 8;
- 整体对齐:结构体的 sizeof 必须是最大成员对齐值的倍数,不够就在尾部补填充;
- 填充归编译器:成员之间、结构体末尾的空洞都是编译器自动插入的,你的代码不能假设它们为零。
对齐的意义一句话:让 CPU 按自然边界读内存,避免跨 cache line 的两次访问和某些架构上的总线错误。
逐字节推导:声明顺序 24 字节。
结构体按 char a、uint64_t b、uint32_t c 声明。从偏移 0 开始排:
- a 是 char,落在偏移 0,占 1 字节;
- b 是 uint64_t,对齐 8,下一个可用偏移 1 不是 8 的倍数,编译器在 1~7 插 7 字节填充,b 落在偏移 8,占 8 字节;
- c 是 uint32_t,对齐 4,偏移 16 恰好是 4 的倍数,c 落在 16,占 4 字节,到偏移 20 结束;
- 整体对齐:当前 20 字节,最大成员对齐值是 8,补齐到 24。
所以 sizeof = 24,其中 11 字节全是填充——将近一半是空气。
重排一下:16 字节。
把成员按对齐值从大到小重排:uint64_t b、uint32_t c、char a。b 在 0 占 8 字节,c 在 8 占 4 字节,a 在 12 占 1 字节,当前 13,整体补齐到 8 的倍数即 16。填充从 11 字节缩到 3 字节,省下 8 字节。一个结构体省 8 字节看似不多,百万级实例的数组就是 8 MB,还附带 cache 命中率的收益。
用编译器实测,不靠口算。
写个小程序验证,clang 编译运行(clang align.c -o align && ./align):
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
struct Before { char a; uint64_t b; uint32_t c; }; // 声明顺序
struct After { uint64_t b; uint32_t c; char a; }; // 大成员靠前
int main(void) {
printf("sizeof(Before) = %zu\n", sizeof(struct Before));
printf("offsetof: a=%zu b=%zu c=%zu\n",
offsetof(struct Before, a), offsetof(struct Before, b),
offsetof(struct Before, c));
printf("sizeof(After) = %zu\n", sizeof(struct After));
printf("offsetof: b=%zu c=%zu a=%zu\n",
offsetof(struct After, b), offsetof(struct After, c),
offsetof(struct After, a));
return 0;
}
输出:
sizeof(Before) = 24
offsetof: a=0 b=8 c=16
sizeof(After) = 16
offsetof: b=0 c=8 a=12
偏移量与逐字节推导完全一致。
图 2:两行字节网格把 24 和 16 的每一格归属画清楚了。
笔试与面试里常见的追问。
一是 #pragma pack(1) 或 __attribute__((packed)) 能取消对齐,结构体变成 13 字节紧凑排列,代价是成员访问可能变慢甚至在个别架构上出错,网络协议封包场景才会用;二是嵌套结构体按「最大成员对齐值」递归算,内层结构体整体作为一个对齐单位;三是重排优化有个边界——如果成员顺序承载语义(比如硬件寄存器映射、协议字段顺序),就不能动,只能靠显式保留字段。笔试遇到四成员、含指针或数组的变体也别慌,规则不变:先算每个成员的落位偏移,再把整体补齐到最大对齐的倍数。
再回答一个笔试卷上的经典变体。
原卷那道题给的是四成员版本——char、uint64_t 加两个 uint32_t——以及一个 char 加两个 uint32_t 的小结构体,让选两个 sizeof。用同样的方法推:第一个 1 加 7 填充加 8 加 4 加 4,正好 24,无需尾填充;第二个 1 加 3 填充加 4 加 4,整体对齐 4,得 12。可见成员越多、大小类型穿插越乱,填充越可观;把两个 uint32_t 挪到一起、char 垫底,两个小结构体都能再省。答题时把每个成员的落位偏移写在草稿纸上,比盯着选项硬猜快得多。
口诀收个尾:成员偏移是自身对齐的倍数,整体大小是最大对齐的倍数,大成员靠前最省。你当年笔试遇到的是几个成员的组合?有没有被 pack 相关的追问难住过?评论区交流。