文件操作(下):格式化读写、二进制读写与随机访问

好,咱们继续聊文件操作。上一章我们把文件的基本打开关闭、字符和字符串读写讲完了。这一章要深入一些——格式化读写、二进制读写,还有随机访问。这几个东西,说白了就是让你跟文件打交道时更灵活、更高效。

我个人习惯把文件操作分成两类:一类是给人看的文本文件,一类是给机器读的二进制文件。两种场景,用的函数完全不同。你想想看,如果你要存一个结构体数组,用 fprintf 一行行写,再一行行解析回来,那效率得多低?

一、格式化读写:fscanf 与 fprintf

这两个函数,跟 scanf/printf 几乎一模一样。区别就在于多了一个文件指针参数。它们的作用,就是按照你指定的格式,从文件里读数据,或者往文件里写数据。

核心区别:fprintf 把格式化的数据写入文件,fscanf 从文件中按格式读取数据。

// 写入格式化数据
FILE *fp = fopen("students.txt", "w");
if (fp == NULL) {
    perror("打开文件失败");
    return -1;
}

fprintf(fp, "%s %d %.2f\n", "张三", 20, 89.5);
fprintf(fp, "%s %d %.2f\n", "李四", 21, 92.0);
fclose(fp);

// 读取格式化数据
fp = fopen("students.txt", "r");
char name[32];
int age;
float score;

while (fscanf(fp, "%s %d %f", name, &age, &score) == 3) {
    printf("姓名: %s, 年龄: %d, 成绩: %.2f\n", name, age, score);
}
fclose(fp);

嗯,这里要注意一个坑。fscanf 在读取失败时不会自动跳过空白字符。我曾经在项目里遇到过一个问题:文件里某一行数据格式不对,fscanf 返回了 2(只成功匹配了两个字段),然后整个循环就卡住了。为什么?因为那个错误的数据还留在缓冲区里,下一次循环又读到了同样的位置,死循环了。

避坑指南:使用 fscanf 时,一定要检查返回值。如果返回值小于期望的字段数,说明数据格式有问题。我建议你加一个错误处理分支,跳过当前行或者直接退出。

二、二进制文件的读写:fread 与 fwrite

二进制读写,才是真正的高效方式。fread 和 fwrite 一次可以读写一整块数据,不需要做任何格式转换。你想想看,如果你要存一个 100 万条记录的结构体数组,用 fprintf 写文本文件,光格式转换的时间就够你喝一壶的。

typedef struct {
    int id;
    char name[32];
    float salary;
} Employee;

// 写入二进制文件
Employee emp = {1001, "王五", 15000.0};
FILE *fp = fopen("employee.dat", "wb");
if (fp == NULL) {
    perror("打开文件失败");
    return -1;
}

size_t written = fwrite(&emp, sizeof(Employee), 1, fp);
if (written != 1) {
    fprintf(stderr, "写入失败\n");
}
fclose(fp);

// 读取二进制文件
Employee read_emp;
fp = fopen("employee.dat", "rb");
if (fp == NULL) {
    perror("打开文件失败");
    return -1;
}

size_t read = fread(&read_emp, sizeof(Employee), 1, fp);
if (read == 1) {
    printf("ID: %d, 姓名: %s, 薪资: %.2f\n", 
           read_emp.id, read_emp.name, read_emp.salary);
}
fclose(fp);

我记得有一次做嵌入式项目,需要在 Flash 里存储设备配置参数。结构体里有整型、浮点、还有字符串数组。用 fprintf 写文本?别闹了,Flash 写入次数有限,而且解析文本太慢了。直接用 fwrite 把整个结构体写进去,读取时 fread 一次性读回来,又快又省空间。

小技巧:fread/fwrite 的第三个参数是“块数”,第四个参数是“每块大小”。我个人习惯把块数设为 1,块大小设为 sizeof(结构体)。这样代码更清晰,也更容易检查错误。

三、文件的随机读写:fseek、ftell、rewind

前面讲的读写方式,都是顺序的——从文件头读到文件尾。但实际项目中,经常需要跳到文件的某个位置去读写。比如一个日志文件,你想直接读最后 100 字节;或者一个数据库文件,你想更新第 N 条记录。

这时候就需要 fseek、ftell 和 rewind 了。

函数 作用 示例
fseek(fp, offset, whence) 将文件指针移动到指定位置 fseek(fp, 0, SEEK_END) 跳到文件末尾
ftell(fp) 返回当前文件指针的位置(相对于文件头) long pos = ftell(fp);
rewind(fp) 将文件指针重置到文件开头 rewind(fp); 等价于 fseek(fp, 0, SEEK_SET)

whence 参数有三个选项:

  • SEEK_SET:从文件头开始计算偏移量
  • SEEK_CUR:从当前位置开始计算偏移量
  • SEEK_END:从文件尾开始计算偏移量
// 获取文件大小
FILE *fp = fopen("data.bin", "rb");
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
printf("文件大小: %ld 字节\n", file_size);

// 跳到第 5 条记录(假设每条记录 100 字节)
fseek(fp, 5 * 100, SEEK_SET);
Record rec;
fread(&rec, sizeof(Record), 1, fp);

// 回到文件开头
rewind(fp);

注意:fseek 对文本文件的处理可能有问题。因为文本文件在 Windows 下会有换行符转换(\n 变成 \r\n),导致实际字节数和逻辑位置不一致。所以随机读写最好用在二进制文件上。

四、知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以看到,文件操作其实就三条主线:格式化读写、二进制读写、随机访问。每条线都有自己的适用场景和注意事项。

文件操作(下)知识体系 格式化读写 二进制读写 随机访问 fprintf:按格式写入 fscanf:按格式读取 适用:文本配置文件、日志 fwrite:整块写入内存 fread:整块读取到内存 适用:结构体、数组、大数据 fseek:定位到任意位置 ftell:获取当前位置 rewind:回到文件开头 核心原则:文本用格式化,数据用二进制,随机访问用 fseek 注意:fseek 在文本文件上可能因换行符转换导致位置偏移

五、综合示例:一个简单的记录管理器

最后,我写一个小例子,把今天讲的三个知识点串起来。这个程序可以往文件里追加记录,也可以按 ID 直接读取某条记录。

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

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

// 追加一条记录
void append_record(const char *filename, Record *rec) {
    FILE *fp = fopen(filename, "ab");
    if (fp == NULL) return;
    fwrite(rec, sizeof(Record), 1, fp);
    fclose(fp);
}

// 按 ID 读取记录(ID 从 0 开始)
int read_record_by_id(const char *filename, int id, Record *rec) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) return -1;
    
    // 直接跳到第 id 条记录
    fseek(fp, id * sizeof(Record), SEEK_SET);
    size_t n = fread(rec, sizeof(Record), 1, fp);
    fclose(fp);
    
    return (n == 1) ? 0 : -1;
}

int main() {
    Record r1 = {0, "张三", 88.5};
    Record r2 = {1, "李四", 92.0};
    Record r3 = {2, "王五", 76.5};
    
    append_record("records.dat", &r1);
    append_record("records.dat", &r2);
    append_record("records.dat", &r3);
    
    Record tmp;
    if (read_record_by_id("records.dat", 1, &tmp) == 0) {
        printf("ID=1: %s, %.2f\n", tmp.name, tmp.score);
    }
    
    return 0;
}

这个例子虽然简单,但已经包含了二进制读写和随机访问的核心用法。你在实际项目中,可以把 Record 换成任何结构体,比如设备参数、网络配置、传感器数据等等。

最后说一句:文件操作最容易出问题的就是路径和权限。我建议你在 fopen 之后立即检查返回值,不要假设文件一定能打开。另外,二进制文件在不同平台之间移植时要注意字节序(大端/小端)问题,这个我们后面讲到跨平台开发时再细聊。


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