28、文件操作(二):字符读写、字符串读写、格式化读写

文件操作这块,上一章我们聊了文件的打开与关闭。今天咱们深入一点,看看文件里具体怎么读写数据。

我个人习惯把文件读写分成三个层次:字符级字符串级格式化级。这三个层次,说白了就是「一个字一个字读」、「一行一行读」和「按格式解析读」。你想想看,实际开发中这三种场景都会遇到,一个都不能少。

文件读写三层次 文件 I/O 字符级 字符串级 格式化级 fgetc / fputc fgets / fputs fscanf / fprintf 逐字节处理 逐行读取 结构化解析 从底层到高层,按需选择 越往上,抽象程度越高,使用越方便

一、字符读写:fgetc 与 fputc

这是最底层的读写方式。一次只操作一个字符。

fputc 的用法很简单:

int fputc(int ch, FILE *stream);

把字符 ch 写入文件流。成功返回写入的字符,失败返回 EOF。

fgetc 反过来:

int fgetc(FILE *stream);

从文件流读取一个字符。读到文件末尾或出错时返回 EOF。

核心要点:EOF 是 -1,而 char 类型可能是 unsigned。所以 fgetc 的返回值用 int 接收,别用 char。我见过不少新手在这上面翻车。

来个实际例子:复制一个文本文件。

#include <stdio.h>

int main() {
    FILE *src = fopen("source.txt", "r");
    FILE *dst = fopen("dest.txt", "w");
    if (!src || !dst) {
        printf("文件打开失败\n");
        return 1;
    }

    int ch;
    while ((ch = fgetc(src)) != EOF) {
        fputc(ch, dst);
    }

    fclose(src);
    fclose(dst);
    return 0;
}

我的经验:字符读写虽然慢,但在处理二进制文件头、解析协议帧时特别好用。我曾经在嵌入式项目里用 fgetc 逐字节解析自定义通信协议,稳得很。

二、字符串读写:fgets 与 fputs

字符读写太慢了。实际开发中,我们更常用字符串读写。

fputs 把字符串写入文件:

int fputs(const char *str, FILE *stream);

成功返回非负数,失败返回 EOF。注意它不会自动加换行符。

fgets 从文件读取一行:

char *fgets(char *str, int n, FILE *stream);

读取最多 n-1 个字符,遇到换行符或 EOF 停止。读取的换行符会保留在字符串里,然后自动加 '\0'。

我曾经踩过的坑:fgets 的第二个参数 n 是缓冲区大小,不是「想读的字符数」。如果你写 fgets(buf, 5, fp),它最多读 4 个字符 + 1 个 '\0'。很多人写成 fgets(buf, strlen(buf), fp),结果 buf 根本没初始化,直接崩了。

看个例子:逐行读取配置文件。

#include <stdio.h>

int main() {
    FILE *fp = fopen("config.ini", "r");
    if (!fp) {
        perror("打开文件失败");
        return 1;
    }

    char line[256];
    int line_num = 0;
    while (fgets(line, sizeof(line), fp)) {
        line_num++;
        // 去掉末尾换行符
        size_t len = strlen(line);
        if (len > 0 && line[len-1] == '\n') {
            line[len-1] = '\0';
        }
        printf("第%d行: %s\n", line_num, line);
    }

    fclose(fp);
    return 0;
}

我个人的习惯:用 fgets 读取后,总是手动去掉末尾的换行符。因为很多字符串处理函数(比如 strcmp)不会忽略 '\n',留着它容易出 bug。

三、格式化读写:fscanf 与 fprintf

这是最高级的读写方式。直接按格式解析或输出数据。

fprintf 和 printf 几乎一样,只是多了一个文件指针参数:

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

fscanf 和 scanf 类似:

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

看个例子:把结构体数据写入文件,再读回来。

#include <stdio.h>

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

int main() {
    Student stu = {1001, "张三", 89.5};

    // 写入文件
    FILE *fp = fopen("student.txt", "w");
    if (!fp) return 1;
    fprintf(fp, "%d %s %.1f\n", stu.id, stu.name, stu.score);
    fclose(fp);

    // 读取回来
    Student stu2 = {0};
    fp = fopen("student.txt", "r");
    if (!fp) return 1;
    fscanf(fp, "%d %s %f", &stu2.id, stu2.name, &stu2.score);
    fclose(fp);

    printf("读取结果: %d %s %.1f\n", stu2.id, stu2.name, stu2.score);
    return 0;
}

注意:fscanf 遇到空格、换行符会自动分隔。如果字符串里包含空格,fscanf 会读不全。这时候用 fgets 更靠谱。

四、三种方式的对比

函数 读写单位 适用场景 注意事项
fgetc / fputc 1 个字符 逐字节处理、协议解析 返回值用 int 接收
fgets / fputs 1 行字符串 文本文件逐行处理 注意缓冲区大小和换行符
fscanf / fprintf 格式化数据 结构化数据读写 字符串含空格时慎用 fscanf

五、避坑指南

我这些年做项目,文件操作踩过的坑不少。挑几个典型的说说:

  • 忘记检查返回值:fopen 返回 NULL 不检查,后面直接读写,程序直接崩。每次打开文件后,养成检查的习惯。
  • 缓冲区溢出:fgets 的第二个参数写错了,或者用 gets(这个函数已经被废弃了)。永远用 fgets,别用 gets。
  • 文件未关闭:fclose 忘记调用,导致文件句柄泄漏。程序跑久了,系统资源耗尽。
  • 文本模式 vs 二进制模式:Windows 下文本模式会把 '\n' 转成 '\r\n',二进制模式不会。跨平台时要注意。

我曾经犯过的错:在一个日志系统里,用 fscanf 读取包含时间戳的日志行。时间戳格式是 "2024-01-15 14:30:00",中间有空格。fscanf 读到空格就停了,后面的数据全乱了。后来改成 fgets + sscanf 才解决。

六、小结

文件读写就这三种方式,从字符到字符串再到格式化,一层比一层方便。实际开发中,我建议:

  • 处理二进制数据或协议帧 → 用 fgetc/fputc
  • 处理文本配置文件、日志文件 → 用 fgets/fputs
  • 处理结构化的数值数据 → 用 fscanf/fprintf

嗯,文件操作这块,多写多练就熟了。记住一点:每次打开文件,都要记得关闭。这是最基本的素养。


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