3、二进制序列化:使用memcpy进行基础序列化、结构体直接写入文件
好,咱们今天聊点实在的。
上一章我们讲了文本序列化,JSON、XML 这些格式虽然可读性好,但有个硬伤——慢。而且体积大。你想想看,一个 int 本来 4 个字节,转成字符串 "123456" 要占 6 个字节,传输和存储都吃亏。
所以,在嵌入式系统里,二进制序列化才是主流。我做了十几年嵌入式,坦白讲,90% 以上的通信协议都是二进制的。为什么?因为快、省、直接。
3.1 什么是二进制序列化?
说白了,就是把内存里的数据,原封不动地搬出去。
你在结构体里定义的 int、float、char 数组,它们在内存里怎么排列的,序列化后就怎么写到文件或发到网络里。反序列化时,再原样读回来。
这就像搬家——你不需要把家具拆成零件再画图纸,直接把整个柜子搬过去就行。
核心思想:内存布局 = 传输格式
没有解析开销,没有格式转换,性能极高。
3.2 用 memcpy 做序列化
最基础的做法,就是 memcpy。把结构体变量的内存内容,直接拷贝到一个字节缓冲区里。
我给你们看个例子:
#include <stdio.h>
#include <string.h>
typedef struct {
int id;
float value;
char name[16];
} SensorData;
int main() {
SensorData data = {1, 23.5f, "temp_sensor"};
unsigned char buffer[sizeof(SensorData)];
// 序列化:把结构体拷贝到缓冲区
memcpy(buffer, &data, sizeof(SensorData));
// 反序列化:从缓冲区拷贝回结构体
SensorData restored;
memcpy(&restored, buffer, sizeof(SensorData));
printf("id=%d, value=%.1f, name=%s\n",
restored.id, restored.value, restored.name);
return 0;
}
你看,就这么简单。两行 memcpy,序列化和反序列化都搞定了。
我的习惯:序列化时用 memcpy 把数据拷到 unsigned char 数组里,而不是直接写结构体到文件。这样更灵活,方便加协议头、校验等。
3.3 结构体直接写入文件
如果你只是想存盘,更粗暴的方式是直接用 fwrite 把整个结构体写进去。
FILE *fp = fopen("sensor.bin", "wb");
if (fp) {
fwrite(&data, sizeof(SensorData), 1, fp);
fclose(fp);
}
// 读回来
fp = fopen("sensor.bin", "rb");
if (fp) {
fread(&restored, sizeof(SensorData), 1, fp);
fclose(fp);
}
嗯,这里要注意:fwrite 和 fread 的第二个参数是单个元素大小,第三个参数是元素个数。我见过有人把这两个参数写反了,结果数据错位,排查了半天。
3.4 避坑指南:结构体对齐
好,现在我要说重点了——结构体对齐。
你想想看,C 语言的结构体在内存里并不是紧密排列的。编译器会在成员之间插入填充字节,让每个成员对齐到它的自然边界。比如 int 要 4 字节对齐,double 要 8 字节对齐。
我曾经在一个项目里吃过这个亏。当时写了一个通信协议,结构体里混了 char、int、short,在 x86 上测试一切正常。结果部署到 ARM 平台后,数据全乱了。为什么?因为两个平台的对齐规则不一样,结构体大小不同。
重要:不同编译器、不同平台,结构体的内存布局可能不同。直接 memcpy 序列化后的数据,在跨平台场景下会出问题。
怎么解决?两个思路:
- 手动填充:在结构体里显式添加填充字节,保证布局可控。
- 使用
#pragma pack:强制编译器取消对齐优化。
看代码:
#pragma pack(push, 1) // 按 1 字节对齐
typedef struct {
char type;
int value;
short flag;
} PackedData;
#pragma pack(pop)
printf("sizeof(PackedData) = %zu\n", sizeof(PackedData));
// 输出:7(1 + 4 + 2),没有填充字节
用了 #pragma pack(1) 之后,结构体就是紧密排列的,跨平台一致性大大提高。但代价是访问效率会降低,因为 CPU 访问未对齐的数据可能需要多次内存访问。
我的建议:在嵌入式系统里,如果数据要跨平台传输,一律用 #pragma pack(1)。性能损失通常可以接受,但数据一致性是底线。
3.5 二进制序列化的优缺点
| 优点 | 缺点 |
|---|---|
| 速度快,几乎没有解析开销 | 数据不可读,调试困难 |
| 体积小,节省存储和带宽 | 跨平台需要处理字节序和对齐 |
| 实现简单,代码量少 | 结构体变化后,旧数据无法兼容 |
| 适合实时性要求高的场景 | 指针类型无法直接序列化 |
3.6 什么时候用二进制序列化?
我个人经验是:
- 嵌入式通信协议:比如 I2C、SPI、CAN 总线,数据包很小,必须用二进制。
- 高性能日志:传感器数据采集,每秒几千个点,用文本写会卡死。
- 固件升级包:二进制镜像,直接烧录,不需要解析。
- 游戏存档:快速读写,玩家体验好。
但如果你需要人类可读、需要版本兼容、需要跨语言交互,那还是老老实实用文本序列化或者 protobuf 吧。
3.7 本章知识体系
下面这张图,帮你理清二进制序列化的核心脉络:
3.8 小结
二进制序列化,说白了就是用内存的视角去看待数据。它简单、高效,但需要你对底层的内存布局有清晰的认识。
我个人的经验是:在同一个平台内部,放心用 memcpy;跨平台时,一定要处理对齐和字节序。别偷懒,否则线上出问题的时候,你连数据都解析不出来。
下一章,我们会深入聊字节序(大小端)的问题——这可是跨平台通信的必修课。
公众号:蓝海资料掘金营,微信deep3321