结构体与内存对齐:内存对齐规则、为什么要对齐、#pragma pack 的使用

说到结构体,有个躲不开的话题——内存对齐。我刚开始学C语言那会儿,觉得结构体就是简单地把几个变量打包在一起,大小不就是各个成员加起来嘛。结果一打印 sizeof,傻眼了,怎么比预想的大那么多?

后来被坑了几次,才老老实实把内存对齐这块啃下来。今天咱们就把它彻底讲明白。

一、什么是内存对齐?

说白了,内存对齐就是编译器在安排结构体成员时,不是紧挨着放的,而是按照某种规则在成员之间插入一些“填充字节”。

举个例子:

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

你觉得 sizeof(struct Test) 是多少?1+4+1=6?不对。在我的机器上,结果是12。为什么会这样?这就是对齐在作怪。

二、内存对齐的规则

对齐规则其实就三条,记住了就能应付绝大多数情况:

  1. 每个成员都有自己的对齐值:对齐值 = min(成员自身大小, 编译器默认对齐值)。在大多数32位/64位系统上,默认对齐值是4或8。
  2. 成员的起始地址必须能被它的对齐值整除:比如 int 是4字节,它的起始地址必须是4的倍数。
  3. 结构体的总大小必须是最大成员对齐值的整数倍:不足就补。

咱们拿上面的结构体分析一下:

  • char a 占1字节,地址0,没问题
  • int b 要对齐到4的倍数,所以从地址4开始,跳过了地址1-3(填充了3字节)
  • char c 占1字节,地址8,没问题
  • 结构体最大对齐值是4(int),总大小必须是4的倍数。目前用了9字节(0-8),补到12

所以结果是12。你想想看,如果成员顺序换一下:

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

这时候 int 从0开始,char a 在地址4,char c 在地址5,总大小6,对齐到4的倍数就是8。比之前的12省了4个字节!

经验之谈:我在项目中遇到过因为结构体成员顺序不合理,导致内存浪费30%的情况。尤其是嵌入式设备,RAM寸土寸金,把大字段放在前面,小字段放在后面,能省不少空间。

三、为什么要对齐?

你可能会问:搞这么麻烦干嘛?直接挨着放不就行了?

原因其实很简单——CPU访问内存的效率

大多数CPU读取内存是按“字”来读的,比如32位CPU一次读4字节。如果 int 变量放在地址1,CPU需要读两次才能拿到完整数据:第一次读地址0-3,取高3字节;第二次读地址4-7,取低1字节,然后拼起来。这多了一次内存访问,还多了拼接操作。

但如果 int 放在地址0或地址4,CPU一次就读完了。

所以内存对齐的本质是:用空间换时间

我记得有一次调试一个通信协议栈,发现同样的数据在不同平台上解析结果不一样。查了半天,就是因为结构体对齐方式不同,导致字段偏移位置变了。从那以后,我只要涉及跨平台通信,一定会显式控制对齐方式。

四、#pragma pack 的使用

有些场景下,我们不想让编译器自动填充。比如:

  • 结构体要写入文件或通过网络发送
  • 要和硬件寄存器一一对应
  • 要节省内存(比如嵌入式设备)

这时候可以用 #pragma pack 来改变对齐方式。

#pragma pack(1)  // 设置对齐值为1字节
struct PackedStruct {
    char a;
    int b;
    char c;
};
#pragma pack()   // 恢复默认对齐

// 此时 sizeof(PackedStruct) = 6,没有填充字节

#pragma pack(n) 的作用是:把默认对齐值改为 n。n 通常是1、2、4、8、16。

规则变成了:每个成员的对齐值 = min(成员自身大小, n)。

比如 #pragma pack(2) 时,int 的对齐值变成 min(4,2)=2,所以 int 只需要对齐到2的倍数即可。

注意#pragma pack 是编译器指令,不是C语言标准。不同编译器可能有差异。GCC、MSVC、ARMCC 都支持,但语法细节略有不同。

五、避坑指南

我曾经在一个项目里吃过亏,分享出来大家引以为戒:

  • 跨平台通信时一定要 pack(1):不同平台默认对齐值不同,不指定的话,结构体在A平台和B平台的大小可能不一样,收发双方对不上。
  • pack 后的结构体访问性能会下降:因为未对齐的成员可能需要多次内存访问。在性能敏感的代码里要权衡。
  • 不要对 pack 后的结构体取成员地址做指针运算:有些CPU(比如ARM)访问未对齐地址会触发异常。
  • 位域和 pack 一起用要小心:位域的存储布局在不同编译器下可能不同,加上 pack 更复杂。

六、知识体系总览

下面这张图把内存对齐的核心知识点串起来了:

结构体内存对齐知识体系 内存对齐 对齐规则 成员对齐值 = min(自身大小, 默认值) 起始地址必须能被对齐值整除 总大小是最大对齐值的整数倍 为什么要对齐 CPU一次读取一个字 未对齐需要两次读取 空间换时间 #pragma pack pack(n) 设置对齐值为 n pack() 恢复默认 用于网络/文件/硬件寄存器 应用场景 跨平台通信 | 嵌入式开发 | 协议解析 | 性能优化

七、总结

内存对齐是C语言里一个“看不见但很重要”的机制。记住三点:

  • 对齐规则:每个成员按自己的对齐值对齐,结构体总大小对齐到最大对齐值的倍数
  • 对齐目的:让CPU访问更高效,用空间换时间
  • 控制对齐:用 #pragma pack 可以改变对齐方式,但要注意性能和兼容性

嗯,这块内容就这些。实际开发中多留意结构体的大小和成员顺序,能帮你省不少调试时间。


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