Alex

彻底搞懂 C 语言内存对齐与结构体优化

  • C语言
  • 内存对齐
  • 结构体

为什么一个只有 charintshort 的结构体,理论数据量只有 7 字节,sizeof 却可能算出 12 字节?

为什么结构体成员只是换个顺序,内存占用就能明显变小?

这篇文章按一条主线展开:硬件为什么需要对齐 → C 语言如何插入 Padding → 结构体成员顺序如何影响大小 → 日常怎么优化


1. 什么是内存对齐?

用一句话概括:数据在内存中的起始地址,通常需要满足某个对齐要求。

更具体一点,若一个对象的对齐要求是 4 字节,那么它的起始地址通常应该是 4 的整数倍;若对齐要求是 8 字节,那么起始地址通常应该是 8 的整数倍。

可以把 CPU 读取内存想象成搬家司机搬箱子。司机不是每次只搬一个字节,而是倾向于按固定宽度成块读取。如果一个 4 字节的 int 正好落在 4 字节边界上,CPU 可以更自然地一次取走;如果它横跨了两个边界,硬件可能需要拆成多次访问再拼接,代价更高,某些架构上甚至不允许这种非对齐访问。

核心结论: 内存对齐不是编译器故意浪费空间,而是 C 语言对象布局与底层硬件访问习惯之间的配合。


2. C 语言里的对齐规则

很多初学者容易误解:既然结构体里最大的成员是 int,是不是所有成员都必须按 4 字节对齐?

不是。每个成员按自己的对齐要求摆放,结构体整体再按最大成员对齐要求收尾。

2.1 成员自身对齐

在常见平台上,基本类型大致有这样的对齐习惯:

类型常见大小常见对齐要求说明
char1 字节1 字节任意地址都天然满足 1 字节对齐
short2 字节2 字节起始地址通常放在 2 的倍数
int4 字节4 字节起始地址通常放在 4 的倍数
double8 字节8 字节起始地址通常放在 8 的倍数

注意这里说的是“常见平台”。C 标准没有承诺所有平台上 int 一定 4 字节、double 一定 8 字节对齐,真实规则由目标平台 ABI、编译器和编译选项共同决定。

如果想在本机验证,可以用 C11 的 alignofsizeof

#include <stdalign.h>
#include <stdio.h>
 
int main(void) {
    printf("char:   size=%zu, align=%zu\n", sizeof(char), alignof(char));
    printf("short:  size=%zu, align=%zu\n", sizeof(short), alignof(short));
    printf("int:    size=%zu, align=%zu\n", sizeof(int), alignof(int));
    printf("double: size=%zu, align=%zu\n", sizeof(double), alignof(double));
    return 0;
}

2.2 结构体尾部对齐

结构体还有一条容易被忽略的规则:结构体总大小通常要补齐到其最大成员对齐要求的整数倍。

这不是为了单个结构体好看,而是为了结构体数组。

struct Item {
    int value;
    char flag;
};
 
struct Item arr[2];

如果 struct Item 的大小不补齐,arr[1] 的起始地址就可能无法满足 int value 的对齐要求。于是编译器会在结构体末尾补 Padding,让数组中每个元素都能从正确边界开始。


3. Padding 是怎么出现的?

结构体成员会按照源码里的声明顺序依次排列。编译器不能随便调换成员顺序,因为那会改变程序员观察到的对象布局,也会破坏 offsetof、二进制协议、文件格式、硬件寄存器映射等场景。

所以当下一个成员的当前位置不满足对齐要求时,编译器只能插入一些空白字节,也就是 Padding。

3.1 一个占用 12 字节的结构体

struct S1 {
    char a;   // 1 字节
    int b;    // 4 字节,通常要求 4 字节对齐
    short c;  // 2 字节,通常要求 2 字节对齐
};

在常见 64 位平台上,struct S1 通常会这样排布:

偏移内容
+0a
+1 ~ +3Padding,为了让 b 从 4 的倍数开始
+4 ~ +7b
+8 ~ +9c
+10 ~ +11尾部 Padding,让结构体总大小成为 4 的倍数

有效数据只有 7 字节,但 sizeof(struct S1) 通常是 12。

可以用 offsetof 验证成员偏移:

#include <stddef.h>
#include <stdio.h>
 
struct S1 {
    char a;
    int b;
    short c;
};
 
int main(void) {
    printf("sizeof(S1) = %zu\n", sizeof(struct S1));
    printf("a = %zu\n", offsetof(struct S1, a));
    printf("b = %zu\n", offsetof(struct S1, b));
    printf("c = %zu\n", offsetof(struct S1, c));
    return 0;
}

3.2 仅仅调整顺序就变小

如果把对齐要求更高的成员放到前面,Padding 往往会减少:

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

常见布局如下:

偏移内容
+0 ~ +3b
+4 ~ +5c
+6a
+7尾部 Padding,让结构体总大小成为 4 的倍数

这时 sizeof(struct S2) 通常是 8。业务字段完全没变,只是成员顺序变化,结构体大小就从 12 字节降到了 8 字节。

核心结论: 结构体优化的第一原则通常是:把对齐要求大的成员放前面,把小成员集中放后面。


4. 为什么不能全部按 8 字节对齐?

既然对齐有利于 CPU 访问,为什么不让所有数据都按 8 字节对齐?

原因很简单:性能和空间需要平衡。

如果所有对象都强行按 8 字节对齐,像 char 这种 1 字节数据也可能被塞进大量 Padding。单个对象看起来浪费不大,但数组、结构体、缓存行里都会出现严重碎片,最终浪费内存,也降低缓存利用率。

如果完全不对齐,CPU 访问某些数据时又可能频繁跨边界,轻则多读几次再拼接,重则在某些架构上触发异常。对并发程序来说,非对齐访问还可能让本该原子完成的读写被拆开,带来数据撕裂等问题。

所以 C 语言对象布局的目标不是“一律最大对齐”,而是让每种类型按它需要的边界摆放,在访问效率和空间利用之间取得平衡。


5. 结构体优化的实用建议

结构体成员顺序不是越随意越好,但也不是所有结构体都值得手工压缩。日常可以遵循几条原则。

  1. 优先按对齐要求从大到小排列 — 例如先放 double、指针、longint,再放 shortchar、布尔标记位。
  2. 把多个小字段集中在一起 — 多个 charbool、枚举标记可以连续放置,减少成员之间的 Padding。
  3. sizeofalignofoffsetof 验证结果 — 不要只靠脑补,不同平台的 ABI 可能不同。
  4. 谨慎使用强制打包#pragma pack 或编译器扩展可以减少 Padding,但可能引入非对齐访问成本,甚至破坏跨平台兼容性。
  5. 不要破坏外部二进制协议 — 如果结构体已经用于文件格式、网络协议、FFI 或硬件寄存器映射,成员顺序不能只为了省内存随便改。

6. 总结

规则要点
成员自身对齐每个成员通常按自己的对齐要求放置
中间 Padding当前偏移不满足下一个成员对齐要求时插入
尾部 Padding结构体大小通常补齐到最大成员对齐要求的整数倍
成员顺序优化对齐要求大的成员放前面,小成员集中放后面
平台差异真实大小和对齐由 ABI、编译器、选项共同决定

日常开发再记两条:

  1. sizeof(struct) 大于所有成员大小之和,通常是 Padding 在起作用。
  2. 优化结构体前,先确认它不是外部二进制接口的一部分;能调整顺序时,再用 sizeofoffsetof 实测。