8. 结构体与文件操作:结构体的序列化与反序列化

好,咱们今天聊点实战的。结构体定义好了,内存里跑得欢,但程序一关,数据全没了。怎么办?写到文件里去。

把结构体数据变成字节流存到文件,这叫序列化。反过来,从文件读回内存重建结构体,叫反序列化。说白了,就是给数据拍个快照,下次开机还能恢复现场。

8.1 为什么需要序列化?

我早年做嵌入式设备时,有个设备每次断电都要重新校准参数。用户烦,我也烦。后来我加了个结构体,把校准参数打包,关机前写进EEPROM,开机再读出来。嗯,问题就解决了。

序列化的典型场景:

  • 配置保存:设备参数、用户设置
  • 数据持久化:传感器日志、状态记录
  • 网络传输:把结构体打包成字节流发出去
  • 进程间通信:共享内存或管道里传结构体

核心思想:结构体在内存中是连续排列的(不考虑对齐的话),所以我们可以直接把内存块整个搬进文件。搬进去是序列化,搬出来是反序列化。

8.2 fwrite / fread 读写结构体

C标准库给了我们两个好用的函数:fwritefread。它们直接操作内存块,非常适合结构体的批量读写。

函数原型:

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
  • ptr:结构体的起始地址
  • size:每个元素的大小(用 sizeof
  • nmemb:要写/读的元素个数(通常是1)
  • stream:文件指针

来看个例子。假设我们有个学生信息结构体:

#include <stdio.h>
#include <string.h>

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

// 序列化:写一个学生到文件
int save_student(const char *filename, const Student *s) {
    FILE *fp = fopen(filename, "wb");
    if (!fp) return -1;

    // 直接写整个结构体
    size_t written = fwrite(s, sizeof(Student), 1, fp);
    fclose(fp);

    return (written == 1) ? 0 : -1;
}

// 反序列化:从文件读一个学生
int load_student(const char *filename, Student *s) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return -1;

    size_t read = fread(s, sizeof(Student), 1, fp);
    fclose(fp);

    return (read == 1) ? 0 : -1;
}

int main() {
    Student stu1 = {1001, "张三", 88.5f};
    save_student("student.dat", &stu1);

    Student stu2;
    load_student("student.dat", &stu2);
    printf("ID: %d, Name: %s, Score: %.1f\n", stu2.id, stu2.name, stu2.score);
    return 0;
}

你看,代码很简洁。但这里有个坑——结构体对齐。我当年就吃过这个亏。

我曾经在A平台用fwrite写了一个结构体,拿到B平台用fread读,结果数据全乱了。为什么?两个平台的编译器对结构体的对齐方式不一样,内存布局不同,直接读写就出问题。

8.3 结构体对齐带来的问题

结构体不是简单地把成员挨个排好。编译器会在成员之间插入填充字节,让每个成员对齐到它的自然边界。比如:

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

你以为大小是 1+4+2=7 字节?实际上可能是 12 字节。因为 int b 要从4字节对齐的地址开始,char a 后面会填充3个字节。

所以,直接 fwrite(&stu, sizeof(stu), 1, fp) 写进去的,包含了这些填充字节。换到另一个平台,填充规则变了,读出来就是垃圾。

8.4 如何安全地序列化?

我有两个建议:

  1. 使用 packed 结构体:告诉编译器不要填充,成员紧挨着排。
  2. 手动逐字段序列化:每个字段单独写,自己控制格式。

方法一:packed 结构体

GCC 和 MSVC 都支持:

// GCC
typedef struct __attribute__((packed)) {
    char a;
    int b;
    short c;
} PackedStruct;

// MSVC
#pragma pack(push, 1)
typedef struct {
    char a;
    int b;
    short c;
} PackedStruct;
#pragma pack(pop)

这样结构体大小就是 7 字节,没有填充。但代价是访问成员时可能变慢(因为CPU不喜欢非对齐访问)。

我个人习惯:只在序列化时用 packed 结构体,平时用普通结构体。或者干脆写一个转换函数,把普通结构体转成 packed 的再写文件。

方法二:逐字段序列化

这是最稳妥的方式。每个字段按固定顺序、固定大小读写:

int save_student_manual(const char *filename, const Student *s) {
    FILE *fp = fopen(filename, "wb");
    if (!fp) return -1;

    // 按字段逐个写,自己控制格式
    fwrite(&s->id, sizeof(s->id), 1, fp);
    fwrite(s->name, sizeof(s->name), 1, fp);
    fwrite(&s->score, sizeof(s->score), 1, fp);

    fclose(fp);
    return 0;
}

int load_student_manual(const char *filename, Student *s) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return -1;

    fread(&s->id, sizeof(s->id), 1, fp);
    fread(s->name, sizeof(s->name), 1, fp);
    fread(&s->score, sizeof(s->score), 1, fp);

    fclose(fp);
    return 0;
}

这样做的好处是:跨平台兼容。你甚至可以加个版本号,以后结构体变了也能兼容旧数据。

8.5 批量读写结构体数组

如果有一堆结构体要存,fwrite/fread 也支持一次搞定:

Student class[30];
// ... 填充数据 ...

// 一次写30个
fwrite(class, sizeof(Student), 30, fp);

// 一次读30个
fread(class, sizeof(Student), 30, fp);

这里 nmemb 参数就是元素个数。底层会连续读写 size * nmemb 字节。

但要注意:如果结构体里有指针,千万别直接这么干!指针存的是地址,换个进程地址就无效了。这种情况需要深度序列化,把指针指向的内容也写进去。

8.6 知识体系总览

下面这张图帮你理清本章的核心逻辑:

结构体序列化与反序列化 内存中的结构体 序列化(fwrite)→ 文件 反序列化(fread)← 文件 结构体对齐问题 packed 结构体 逐字段序列化 跨平台时:避免直接 fwrite 整个结构体 推荐手动逐字段序列化 + 版本号

8.7 避坑指南

最后,把我这些年踩过的坑总结一下:

  • 不要直接写带指针的结构体:地址是运行时分配的,换个进程就无效。要写指针指向的内容。
  • 注意字节序:大端小端问题。如果数据要在不同架构的机器间交换,建议用网络字节序(htons/htonl)。
  • 文件打开模式用 "wb" 和 "rb":文本模式会在 Windows 下把 \n 转成 \r\n,破坏二进制数据。
  • 检查返回值:fwrite/fread 返回实际写入/读取的元素个数,一定要检查是否等于期望值。
  • 考虑版本兼容:在文件头加个版本号字段,以后结构体变了也能识别旧格式。

我建议:对于嵌入式系统,如果存储空间和性能允许,优先用逐字段序列化。虽然代码多几行,但换来的是跨平台无忧。如果追求极致性能,用 packed 结构体,但一定要在项目文档里注明对齐方式。

好了,结构体与文件操作就聊到这儿。记住:序列化不是简单的内存拷贝,要考虑对齐、字节序、指针和版本兼容。把这些想清楚,你的数据就能在文件里安家落户了。


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