指针与内存对齐:结构体对齐、指针访问对齐内存

内存对齐这个话题,说实话,很多C语言开发者一开始都不太在意。我早年做嵌入式开发时,也踩过这个坑。那时候调试一个通信协议栈,数据总是莫名其妙地丢字节,查了三天才发现是结构体对齐搞的鬼。从那以后,我对内存对齐就再也不敢马虎了。

什么是内存对齐?

内存对齐,说白了就是CPU访问内存时,对地址有「偏好」。比如一个4字节的int,它最好放在能被4整除的地址上。如果放错了位置,CPU就得折腾两次才能读完,性能直接打折扣。

为什么会这样?因为现代CPU读取内存,不是按字节来的,而是按「字」来的。32位处理器一次读4字节,64位一次读8字节。如果数据跨了边界,就得读两次再拼起来。你想想看,这多麻烦。

核心原则:变量的地址必须是其自身大小的整数倍。

  • char(1字节):任何地址都行
  • short(2字节):地址必须是2的倍数
  • int(4字节):地址必须是4的倍数
  • double(8字节):地址必须是8的倍数

结构体对齐的规则

结构体对齐比单个变量复杂一些。我习惯把它拆成两条规则来记:

  1. 成员对齐:每个成员都要放在自己对齐要求的地址上
  2. 整体对齐:结构体总大小必须是最大成员对齐值的整数倍

来看个例子:

struct Example {
    char a;    // 1字节,偏移0
    int b;     // 4字节,偏移4(不是1!)
    short c;   // 2字节,偏移8
};
// 总大小:12字节(不是7字节)

为什么不是1+4+2=7?因为int b需要4字节对齐,所以a后面空了3个字节。这就是传说中的「填充字节」。我在项目中遇到过类似情况,一个看起来很小的结构体,实际占用的内存比想象中大得多。

小技巧:把大的成员往前放,小的往后放,可以减少填充字节。比如上面的例子,改成int、short、char的顺序,大小就变成8字节了。

指针访问对齐内存

指针访问对齐内存,这里有个容易翻车的地方。我曾经写过一个网络数据包解析程序,直接把字节流强制转换成结构体指针:

char buffer[100];
// 假设buffer里存了网络数据包
struct Packet *pkt = (struct Packet *)buffer;  // 危险操作!
pkt->length;  // 可能崩溃

为什么危险?因为buffer的地址可能不是4字节对齐的。如果结构体里有int成员,访问时就可能触发「总线错误」。在ARM平台上,这种错误直接导致程序崩溃。在x86上虽然能跑,但性能会下降。

避坑指南:我曾经在STM32上调试一个传感器驱动,就是因为指针没对齐,程序跑着跑着就进HardFault了。后来用memcpy逐字段拷贝才解决。

正确的做法有两种:

  1. 使用memcpy:把数据拷贝到对齐的结构体变量中
  2. 使用对齐属性:告诉编译器不要对齐(但会牺牲性能)
// 方法1:memcpy安全拷贝
struct Packet pkt;
memcpy(&pkt, buffer, sizeof(pkt));

// 方法2:使用__attribute__((packed))
struct __attribute__((packed)) Packet {
    char type;
    int length;
    short crc;
};

对齐与性能的权衡

对齐不是绝对的。我个人的经验是:

  • 性能优先:保持默认对齐,让CPU跑得最快
  • 空间优先:用packed压缩结构体,但访问速度会慢
  • 兼容性优先:网络协议、文件格式等跨平台场景,必须用packed

嗯,这里要注意:packed虽然省空间,但访问未对齐成员时,编译器会生成额外的代码来处理。在性能敏感的代码里,我建议还是用memcpy。

知识体系图

下面这张图总结了指针与内存对齐的核心逻辑:

指针与内存对齐知识体系 对齐规则 成员对齐:地址是自身大小的倍数 整体对齐:总大小是最大成员的倍数 填充字节:编译器自动插入 指针访问 强制转换可能出问题 memcpy安全拷贝 packed属性压缩 性能权衡 默认对齐:速度最快 packed:节省空间 跨平台:必须packed 核心原则:理解对齐规则,安全使用指针 避免强制转换,优先使用memcpy

实际项目中的经验

我在做嵌入式项目时,总结了几条实用经验:

  • 协议解析:网络包、文件头等,一律用packed结构体,或者逐字段解析
  • 共享内存:多核通信时,注意对齐要求,否则数据可能错位
  • DMA传输:缓冲区地址必须对齐,否则DMA控制器会罢工

调试技巧:用offsetof宏可以查看成员的偏移量,用sizeof看结构体总大小。这两个宏在调试对齐问题时特别好用。

最后说一句:内存对齐不是玄学,是硬件设计决定的。理解了CPU的读取方式,对齐规则就变得很自然了。下次写结构体时,多想想成员怎么排,指针怎么用,能省不少调试时间。


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