8、数据块读写函数:fread与fwrite函数详解、结构体数据的读写、二进制文件操作

说到文件操作,很多初学者喜欢用 fprintffscanf,觉得方便。嗯,确实方便,但效率嘛……说实话,在嵌入式领域,我很少用它们来读写大量数据。为什么?因为文本解析太慢了,而且容易出精度问题。

真正干活的时候,我们更常用 freadfwrite。这两个函数,说白了就是「内存搬运工」——直接把内存里的一整块数据,原封不动地搬到文件里,或者反过来。今天我就带你把它彻底搞明白。

8.1 fwrite:把内存数据「拍」进文件

先看函数原型:

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

参数解释一下:

  • ptr:你要写的数据的起始地址
  • size:每个数据元素的大小(字节数)
  • count:要写多少个这样的元素
  • stream:文件指针

返回值是实际写入的元素个数。如果跟 count 不一样,说明出错了。

举个例子,写一个整型数组:

#include <stdio.h>

int main() {
    int data[] = {100, 200, 300, 400, 500};
    FILE *fp = fopen("data.bin", "wb");
    if (!fp) {
        perror("打开文件失败");
        return 1;
    }

    size_t written = fwrite(data, sizeof(int), 5, fp);
    printf("实际写入 %zu 个元素\n", written);

    fclose(fp);
    return 0;
}

这里我用了 "wb" 模式——b 表示二进制模式。在 Windows 下,如果不加 b,系统会把 0x0A(换行符)自动转成 0x0D 0x0A,数据就坏了。Linux 下没这个问题,但我建议你养成习惯,操作二进制文件一律加 b

我的习惯:只要不是纯文本文件,打开模式一定带 b。哪怕在 Linux 上没区别,也写——这样代码跨平台时不会踩坑。

8.2 fread:从文件「吸」回内存

有写就有读,fread 是它的好搭档:

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数含义跟 fwrite 几乎一样,只是数据流向相反。注意 ptr 指向的内存空间要足够大,否则会缓冲区溢出——这是 C 语言的老大难问题。

接着上面的例子,把数据读回来:

#include <stdio.h>

int main() {
    int buffer[5] = {0};
    FILE *fp = fopen("data.bin", "rb");
    if (!fp) {
        perror("打开文件失败");
        return 1;
    }

    size_t read_count = fread(buffer, sizeof(int), 5, fp);
    printf("实际读取 %zu 个元素\n", read_count);

    for (int i = 0; i < 5; i++) {
        printf("buffer[%d] = %d\n", i, buffer[i]);
    }

    fclose(fp);
    return 0;
}

输出应该跟写入时一模一样。这就是二进制文件的好处——没有精度损失,没有格式转换,快得很。

注意:fread 的返回值是实际读取的元素个数,不是字节数。如果读到文件末尾,返回值会小于 count。我曾经见过有人用 feof() 判断文件结束,结果多读了一次——正确做法是检查返回值。

8.3 结构体数据的读写

这才是 fread/fwrite 的真正用武之地。你想啊,一个结构体里可能有 int、float、char 数组,如果用文本方式读写,你得自己写序列化代码,烦不烦?

直接上代码:

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

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

int main() {
    // 写入
    Student stu1 = {1001, "张三", 95.5f};
    FILE *fp = fopen("student.bin", "wb");
    if (!fp) return 1;

    fwrite(&stu1, sizeof(Student), 1, fp);
    fclose(fp);

    // 读取
    Student stu2 = {0};
    fp = fopen("student.bin", "rb");
    if (!fp) return 1;

    fread(&stu2, sizeof(Student), 1, fp);
    fclose(fp);

    printf("ID: %d, 姓名: %s, 成绩: %.1f\n",
           stu2.id, stu2.name, stu2.score);

    return 0;
}

看到没?一个 fwrite 就把整个结构体写进去了,一个 fread 就全读回来了。简单粗暴,但有效。

核心要点:结构体在内存中是连续存放的(不考虑对齐的话),所以可以整体读写。但要注意——如果结构体里有指针,那写进去的是指针值,不是指针指向的数据。读回来时指针指向的地址早就失效了。

我在项目中遇到过这个问题。有个同事把链表节点直接 fwrite 到文件里,结果读回来一访问就段错误。嗯,教训深刻。

8.4 二进制文件操作的核心要点

二进制文件跟文本文件最大的区别是什么?

  • 文本文件:可读性好,但解析慢,有精度损失(比如 float 转成字符串再转回来,可能差一点点)
  • 二进制文件:不可读,但读写快,数据完全保真

什么时候用二进制?我总结了几条:

  1. 数据量大:比如传感器采集的 10 万个 float 数据,用文本写出来文件巨大,读写也慢
  2. 需要精确还原:比如配置参数、状态快照,不能有精度损失
  3. 嵌入式场景:Flash 空间有限,二进制文件更紧凑

但二进制文件也有缺点——跨平台时要注意字节序(大小端)和结构体对齐。比如在 x86 上写的数据,拿到 ARM 上读,可能对不上。解决办法是定义明确的协议,或者用网络字节序转换函数。

避坑指南:我曾经在 STM32 上写了一个配置结构体到 Flash,结果换了个芯片型号就读不出来了——因为结构体对齐方式变了。后来我改用固定字节序 + 手动打包的方式,才彻底解决。

8.5 知识体系图

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

数据块读写核心知识体系 fread / fwrite 函数原型与参数 返回值检查 二进制模式 'b' 结构体整体读写 指针成员陷阱 对齐与字节序 vs 文本文件对比 适用场景判断 跨平台注意事项 核心原则:数据量大用二进制,跨平台注意字节序

8.6 综合示例:学生成绩管理系统

最后来个综合例子,把今天讲的东西串起来。这个程序演示了如何用二进制文件保存和读取结构体数组:

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

#define MAX_STUDENTS 100

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

// 写入学生数据到二进制文件
int save_students(const char *filename, Student *students, int count) {
    FILE *fp = fopen(filename, "wb");
    if (!fp) return -1;

    size_t written = fwrite(students, sizeof(Student), count, fp);
    fclose(fp);

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

// 从二进制文件读取学生数据
int load_students(const char *filename, Student *students, int max_count) {
    FILE *fp = fopen(filename, "rb");
    if (!fp) return -1;

    size_t read_count = fread(students, sizeof(Student), max_count, fp);
    fclose(fp);

    return (int)read_count;
}

int main() {
    Student class1[] = {
        {1001, "张三", 92.5f},
        {1002, "李四", 88.0f},
        {1003, "王五", 76.5f}
    };
    int count = 3;

    // 保存
    if (save_students("class.bin", class1, count) == 0) {
        printf("保存成功,共 %d 条记录\n", count);
    } else {
        printf("保存失败\n");
        return 1;
    }

    // 读取
    Student loaded[MAX_STUDENTS];
    int loaded_count = load_students("class.bin", loaded, MAX_STUDENTS);
    if (loaded_count > 0) {
        printf("读取成功,共 %d 条记录:\n", loaded_count);
        for (int i = 0; i < loaded_count; i++) {
            printf("  ID: %d, 姓名: %s, 成绩: %.1f\n",
                   loaded[i].id, loaded[i].name, loaded[i].score);
        }
    } else {
        printf("读取失败\n");
    }

    return 0;
}

这个例子在实际项目中很常见。比如嵌入式设备里保存配置参数、传感器校准数据,都是用类似的方式。你想想看,如果每次开机都要重新校准传感器,那多麻烦?

好了,关于 freadfwrite 就讲到这里。记住一句话:二进制文件读写,核心就是「内存块搬运」。搞清楚了这一点,后面学文件随机读写、内存映射文件,都会轻松很多。


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