63. 字节对齐:#pragma pack 的作用

说到字节对齐,我估计不少朋友都吃过它的亏。

结构体里明明只定义了几个小变量,用 sizeof 一量,结果比预想的大一圈。我当时刚入行时也纳闷过——这多出来的空间到底哪来的?

嗯,今天咱们就把 #pragma pack 这个“对齐开关”彻底讲明白。

什么是字节对齐?

说白了,CPU 读取内存不是按字节一个一个读的。它更喜欢按“块”来读——比如 4 字节一块、8 字节一块。如果数据刚好落在这个“块”的边界上,一次就读完了。如果跨了边界,就得读两次,甚至还要拼数据。

编译器为了性能,会自动在结构体成员之间插入一些“填充字节”,让每个成员都对齐到它自然边界上。这就是默认的字节对齐。

核心概念: 对齐 = 让变量的起始地址是它自身大小的整数倍。
比如 int 占 4 字节,它的地址最好是 4 的倍数。

默认对齐的“坑”

看个例子:

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

你觉得 sizeof(struct Test) 是多少?

直觉上:1 + 4 + 2 = 7 字节。但实际跑一下,大概率是 12

为什么会这样?

  • a 占 1 字节,地址 0
  • b 是 int,需要 4 字节对齐。所以它不能紧跟在 a 后面(地址 1 不是 4 的倍数),编译器会在 a 后面填 3 个字节,让 b 从地址 4 开始
  • c 是 short,2 字节对齐,从地址 8 开始,占 2 字节
  • 最后,整个结构体的大小必须是最大成员(int,4 字节)的整数倍。目前用了 10 字节,补到 12

你看,白白浪费了 5 个字节。这在嵌入式系统里可是大事——RAM 本来就金贵。

#pragma pack 登场

#pragma pack 就是用来“收紧”对齐规则的。它告诉编译器:别按默认的那么奢侈了,按我指定的字节数来对齐。

#pragma pack(1)  // 按 1 字节对齐,也就是“紧凑模式”
struct Test {
    char  a;
    int   b;
    short c;
};
#pragma pack()   // 恢复默认对齐

加了 pack(1) 之后,sizeof(struct Test) 就变成 7 了。成员之间没有任何填充,一个紧挨着一个。

我的习惯: 在定义通信协议、文件头、或者需要与硬件寄存器一一对应的结构体时,我几乎必用 #pragma pack(1)。否则两边对齐方式不一样,数据解析出来全是乱的。

pack 的常用值

pack 值 对齐规则 典型场景
1 所有成员按 1 字节对齐,无填充 网络协议、文件格式、硬件寄存器
2 按 2 字节对齐,short/int 可能被压缩 某些 DSP 或 16 位 MCU
4 按 4 字节对齐,接近默认行为 ARM Cortex-M 系列常用
8 按 8 字节对齐,64 位系统默认 x86-64、ARMv8 等

pack 的副作用

凡事都有代价。紧凑对齐省了空间,但可能牺牲性能。

比如一个 int 被放在奇数地址上,CPU 要读两次才能拿到完整数据。在性能敏感的循环里,这种惩罚是肉眼可见的。

注意: 有些 ARM 芯片根本不允许非对齐访问。一旦你 pack(1) 之后去访问一个 int 指针,直接触发硬件异常(HardFault)。我曾经在 STM32 上踩过这个坑,排查了半天才发现是结构体对齐搞的鬼。

实战建议

  • 通信协议: 必须用 pack(1),保证双方结构体布局一致
  • 存储结构: 如果结构体要写入 Flash 或文件,建议用 pack(1),避免版本升级后对齐方式变化导致数据不兼容
  • 性能敏感: 尽量别用 pack(1),或者手动把成员按大小从大到小排列,减少填充
  • 跨平台: 不同编译器对 #pragma pack 的支持略有差异,移植时注意测试

知识体系图

下面这张图帮你理清字节对齐的核心逻辑:

字节对齐核心逻辑 字节对齐 默认对齐(编译器自动) #pragma pack(n) 优点:性能好 缺点:浪费空间 优点:节省空间 缺点:性能下降 权衡:空间 vs 性能 通信协议用 pack(1),计算密集用默认对齐

避坑指南

我曾经在做一个 Bootloader 升级协议时,结构体里定义了一个 4 字节的 CRC 字段。本地测试一切正常,发到另一款芯片上就 CRC 校验失败。查了两天才发现——对方编译器默认是 4 字节对齐,而我这边用了 pack(1),两边结构体布局差了 3 个字节。

从那以后,我养成了一个习惯:只要结构体跨模块、跨芯片、跨语言传递,一定在定义处显式声明对齐方式,并且用静态断言检查 sizeof

// 推荐做法
#pragma pack(1)
typedef struct {
    uint8_t  type;
    uint32_t length;
    uint8_t  data[64];
    uint32_t crc;
} Packet;
#pragma pack()

// 编译时检查
_Static_assert(sizeof(Packet) == 73, "Packet size mismatch!");

嗯,这样就算以后有人改了对齐方式,编译阶段就能发现问题,不用等到联调时抓狂。

字节对齐这事,说大不大,说小不小。但一旦出问题,往往就是那种“看起来都对,跑起来就错”的疑难杂症。希望今天的内容能帮你少走一些弯路。


公众号:蓝海资料掘金营,微信deep3321