4. 文件的顺序读写(二):fprintf、fscanf、fwrite、fread 函数的使用

好,咱们接着聊文件读写。上一节我们讲了 fgetcfputcfgetsfputs,那都是按字符或按行来操作。但实际开发中,我们经常要处理格式化数据,或者直接读写二进制块。这时候,就得请出今天这四位主角了。

我个人习惯把 fprintffscanf 叫做“格式化兄弟”,把 fwritefread 叫做“二进制双雄”。它们各有各的脾气,用错了地方,程序就会给你颜色看。

4.1 fprintf 和 fscanf:格式化读写

这两个函数,说白了就是 printfscanf 的文件版。你想想看,printf 是把数据打印到屏幕上,fprintf 就是把数据打印到文件里。一模一样,就是多了一个文件指针参数。

int fprintf(FILE *stream, const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);

嗯,这里要注意:fscanf 的返回值是成功匹配并赋值的参数个数。如果读到文件末尾,返回 EOF。我在项目中遇到过有人忘了检查这个返回值,结果程序一直用脏数据在跑,查了半天才发现。

核心区别:

  • fprintf 把内存中的数据按格式转换成字符串,写入文件
  • fscanf 从文件中读取字符串,按格式解析成对应的数据类型

来看个实际例子。假设我们要保存一批学生的成绩记录:

#include <stdio.h>

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

int main() {
    Student stu = {"张三", 1001, 89.5f};
    FILE *fp = fopen("students.txt", "w");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    // 写入格式化数据
    fprintf(fp, "%s %d %.1f\n", stu.name, stu.id, stu.score);
    fclose(fp);

    // 读取回来
    Student read_stu;
    fp = fopen("students.txt", "r");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    fscanf(fp, "%s %d %f", read_stu.name, &read_stu.id, &read_stu.score);
    printf("读取结果:%s %d %.1f\n", read_stu.name, read_stu.id, read_stu.score);
    fclose(fp);

    return 0;
}

避坑指南:我曾经在解析日志文件时,用 fscanf 读取字符串,结果忘了指定宽度限制。用户输入了一个超长名字,直接导致缓冲区溢出,程序崩溃。记住:%s 一定要配合宽度使用,比如 %19s,给终止符留个位置。

4.2 fwrite 和 fread:二进制读写

格式化读写虽然方便,但有个问题:它要经过字符串转换,效率不高。而且浮点数经过格式化再解析,精度会有损失。你想想看,如果我要保存一个结构体数组,用 fprintf 一条条写,那得多慢?

这时候 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:每个元素的大小(字节数)
  • nmemb:要读写多少个元素
  • stream:文件指针

返回值是成功读写的元素个数。正常情况下应该等于 nmemb。如果小于 nmemb,说明出错了或者读到文件尾了。

为什么用二进制?

  • 速度快:没有格式转换开销
  • 精度高:浮点数、结构体保持原始内存布局
  • 体积小:不需要存储额外的分隔符和格式字符

来看个完整的例子,把结构体数组写入文件再读回来:

#include <stdio.h>

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

int main() {
    Student students[3] = {
        {"张三", 1001, 89.5f},
        {"李四", 1002, 92.0f},
        {"王五", 1003, 78.5f}
    };

    // 写入二进制文件
    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    size_t written = fwrite(students, sizeof(Student), 3, fp);
    if (written != 3) {
        fprintf(stderr, "写入失败,只写入了 %zu 个元素\n", written);
    }
    fclose(fp);

    // 读取二进制文件
    Student read_students[3];
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    size_t read = fread(read_students, sizeof(Student), 3, fp);
    if (read != 3) {
        fprintf(stderr, "读取失败,只读取了 %zu 个元素\n", read);
    }
    fclose(fp);

    // 打印验证
    for (int i = 0; i < 3; i++) {
        printf("学生%d: %s %d %.1f\n", i+1,
               read_students[i].name,
               read_students[i].id,
               read_students[i].score);
    }

    return 0;
}

重要提醒:二进制文件不能跨平台直接使用。不同机器的字节序(大端/小端)、结构体对齐方式、基本数据类型大小都可能不同。我在做嵌入式项目时,就吃过这个亏——在 x86 上写的二进制文件,放到 ARM 板子上读出来全是乱码。解决方案是:要么统一用网络字节序(htonl/ntohl),要么用格式化文本。

4.3 四种函数的对比与选择

好,咱们把今天这四位放在一起比一比。我画了一张图,帮你理清思路:

文件读写函数选择指南 fprintf 格式化写入 fscanf 格式化读取 fwrite 二进制写入 fread 二进制读取 文本模式 • 人类可读 • 跨平台性好 • 速度较慢 • 浮点精度可能损失 • 适合配置文件、日志 • 需要处理格式字符串 二进制模式 • 机器可读 • 跨平台性差 • 速度快 • 保持原始精度 • 适合大数据、结构体 • 注意字节序和内存对齐 选择原则:需要人类阅读 → 文本模式;追求性能/精度 → 二进制模式

我个人的经验是:

  • 配置文件、日志文件、跨系统交换数据:用 fprintf/fscanf,文本格式通用性强
  • 程序内部数据持久化、大数据量读写:用 fwrite/fread,效率高、精度好
  • 混合使用:有时候我也会在同一个文件里混用,比如头部用文本存元数据,后面用二进制存数据块。但要注意文件指针的位置管理。

4.4 常见错误与调试技巧

最后,分享几个我踩过的坑:

错误1:忘记以二进制模式打开文件

在 Windows 上,文本模式下 \n 会被转换成 \r\n。如果你用 fwrite 写二进制数据,遇到字节值为 0x0A 的,就会被自动插入一个 0x0D,导致数据损坏。记住:二进制读写一定要用 "wb""rb"

错误2:结构体中有指针成员

我曾经直接 fwrite 了一个包含 char * 指针的结构体。结果写进去的是指针地址,不是字符串内容。下次程序重启,那个地址早就失效了。解决方案:要么用定长数组,要么单独序列化字符串。

调试小技巧:如果你不确定文件里写的是什么,可以用 od -c(Linux)或 hexdump 查看二进制内容。我经常用这个来验证 fwrite 是否写对了。另外,feof()ferror() 这两个函数,在读写出错时一定要检查,别偷懒。

好了,这四位函数就讲到这里。记住:格式化读写适合人看,二进制读写适合机器看。选对了工具,你的代码才能跑得又快又稳。


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