指针与内存对齐:结构体对齐、指针访问对齐内存
内存对齐这个话题,说实话,很多C语言开发者一开始都不太在意。我早年做嵌入式开发时,也踩过这个坑。那时候调试一个通信协议栈,数据总是莫名其妙地丢字节,查了三天才发现是结构体对齐搞的鬼。从那以后,我对内存对齐就再也不敢马虎了。
什么是内存对齐?
内存对齐,说白了就是CPU访问内存时,对地址有「偏好」。比如一个4字节的int,它最好放在能被4整除的地址上。如果放错了位置,CPU就得折腾两次才能读完,性能直接打折扣。
为什么会这样?因为现代CPU读取内存,不是按字节来的,而是按「字」来的。32位处理器一次读4字节,64位一次读8字节。如果数据跨了边界,就得读两次再拼起来。你想想看,这多麻烦。
核心原则:变量的地址必须是其自身大小的整数倍。
- char(1字节):任何地址都行
- short(2字节):地址必须是2的倍数
- int(4字节):地址必须是4的倍数
- double(8字节):地址必须是8的倍数
结构体对齐的规则
结构体对齐比单个变量复杂一些。我习惯把它拆成两条规则来记:
- 成员对齐:每个成员都要放在自己对齐要求的地址上
- 整体对齐:结构体总大小必须是最大成员对齐值的整数倍
来看个例子:
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逐字段拷贝才解决。
正确的做法有两种:
- 使用memcpy:把数据拷贝到对齐的结构体变量中
- 使用对齐属性:告诉编译器不要对齐(但会牺牲性能)
// 方法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。
知识体系图
下面这张图总结了指针与内存对齐的核心逻辑:
实际项目中的经验
我在做嵌入式项目时,总结了几条实用经验:
- 协议解析:网络包、文件头等,一律用packed结构体,或者逐字段解析
- 共享内存:多核通信时,注意对齐要求,否则数据可能错位
- DMA传输:缓冲区地址必须对齐,否则DMA控制器会罢工
调试技巧:用offsetof宏可以查看成员的偏移量,用sizeof看结构体总大小。这两个宏在调试对齐问题时特别好用。
最后说一句:内存对齐不是玄学,是硬件设计决定的。理解了CPU的读取方式,对齐规则就变得很自然了。下次写结构体时,多想想成员怎么排,指针怎么用,能省不少调试时间。
公众号:蓝海资料掘金营,微信deep3321