18、文本文件读写:fgetc/fputc、fgets/fputs、fscanf/fprintf

文本文件读写,说白了就是C语言和磁盘上的「纯文本」打交道的方式。我做了这么多年嵌入式开发,发现很多新手一上来就喜欢用格式化读写,觉得方便。但实际项目中,逐字符和逐行读写才是用得最多的——尤其是在处理日志、配置文件、协议报文这些场景下。

今天咱们就把这三组函数掰开揉碎了讲清楚。你想想看,文件IO说白了就三种粒度:一个字符、一行文本、格式化数据。对应到C标准库,就是fgetc/fputc、fgets/fputs、fscanf/fprintf这三对。

核心结论先放这:

  • 逐字符读写:最底层,适合流式处理、协议解析
  • 逐行读写:最常用,适合日志、配置、文本处理
  • 格式化读写:最方便,但性能最差,容易踩坑

18.1 逐字符读写:fgetc 和 fputc

这两个函数是最基础的。fgetc从文件流中读一个字符,fputc往文件流中写一个字符。返回值是int,不是char——这一点我见过太多人栽跟头了。

#include <stdio.h>

// 逐字符复制文件
int copy_file_char_by_char(const char *src, const char *dst) {
    FILE *fp_src = fopen(src, "r");
    FILE *fp_dst = fopen(dst, "w");
    int ch;  // 注意:必须是int,不是char!

    if (!fp_src || !fp_dst) {
        perror("文件打开失败");
        return -1;
    }

    while ((ch = fgetc(fp_src)) != EOF) {
        fputc(ch, fp_dst);
    }

    fclose(fp_src);
    fclose(fp_dst);
    return 0;
}

⚠️ 避坑指南:我曾经在解析一个二进制协议时,用char类型接收fgetc的返回值。结果文件里有个0xFF字节,char类型把它当成-1,直接和EOF相等了——循环提前退出,丢了一整段数据。记住:fgetc返回int,EOF是-1,而0xFF在int里是255,不会冲突。

逐字符读写的适用场景其实很窄。我个人习惯只在以下情况用它:

  • 需要精确控制每个字符的处理(比如转义字符解析)
  • 文件大小很小,或者对内存极度敏感
  • 实现自定义的文本解析器(比如我做过的一个简易JSON解析器)

其他时候,逐字符读写效率太低——每次调用都要陷入内核态,磁盘IO次数爆炸。你想想看,一个1MB的文件,逐字符读要调用100万次fgetc,这谁受得了?

18.2 逐行读写:fgets 和 fputs

这才是文本文件读写的「主力军」。fgets读一行,fputs写一行。注意:fgets会读取换行符并保留在缓冲区中,而fputs不会自动追加换行符。

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

#define LINE_BUF_SIZE 1024

// 逐行读取配置文件,跳过注释和空行
void read_config(const char *filename) {
    FILE *fp = fopen(filename, "r");
    char line[LINE_BUF_SIZE];

    if (!fp) {
        perror("打开配置文件失败");
        return;
    }

    while (fgets(line, sizeof(line), fp)) {
        // 去掉行尾的换行符
        line[strcspn(line, "\n")] = '\0';

        // 跳过空行和注释行
        if (line[0] == '\0' || line[0] == '#') {
            continue;
        }

        printf("配置项: %s\n", line);
    }

    fclose(fp);
}

💡 经验之谈:fgets的第二个参数是缓冲区大小,包括结尾的'\0'。也就是说,如果缓冲区是1024字节,最多只能读1023个字符。我建议缓冲区至少设成1024,太小了容易读到不完整的行。我在项目中处理过一些奇葩的日志文件,有的行长达8000字符——这时候要么动态分配缓冲区,要么用getline(POSIX扩展)。

fputs和fgets配对使用。它不会自动加换行符,所以如果你要写一行完整的文本,记得手动加'\n':

fputs("这是一行日志\n", fp);  // 手动加换行
fputs("这是另一行\n", fp);

逐行读写的优势很明显:

  • 缓冲区可控,不会爆内存
  • 天然按行处理,符合人类阅读习惯
  • 性能比逐字符好得多(一次读一批)

缺点也有:如果一行超长,fgets会截断,下次调用继续读剩下的——这可能导致逻辑错误。我建议在读取关键数据时,检查一下缓冲区末尾有没有换行符,如果没有,说明行被截断了。

18.3 格式化读写:fscanf 和 fprintf

这两个函数是「瑞士军刀」,但也是「双刃剑」。fprintf写格式化数据很方便,fscanf读格式化数据则是个大坑。

#include <stdio.h>

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

// 写入学生信息
void write_student(FILE *fp, const Student *s) {
    fprintf(fp, "%d,%s,%.2f\n", s->id, s->name, s->score);
}

// 读取学生信息(注意:这是有问题的版本)
int read_student_bad(FILE *fp, Student *s) {
    return fscanf(fp, "%d,%[^,],%f\n", &s->id, s->name, &s->score);
}

// 读取学生信息(改进版)
int read_student_good(FILE *fp, Student *s) {
    int ret = fscanf(fp, "%d,%63[^,],%f\n", &s->id, s->name, &s->score);
    if (ret != 3) {
        // 解析失败,需要处理错误
        return -1;
    }
    return 0;
}

⚠️ 避坑指南:我曾经在一个数据采集项目中用fscanf读CSV文件,结果某天数据里出现了一个空字段(比如"1,,85.5"),fscanf直接卡死,整个采集程序挂掉了。从那以后,我定了个规矩:生产环境里,能用fgets+sscanf就别直接用fscanf。为什么?因为fscanf遇到格式不匹配时,会停留在错误位置,后续所有读取都乱套了。而fgets先读一行,再用sscanf解析,至少能保证每行独立处理。

fprintf相对安全一些,但也要注意:

  • 格式化字符串里不要拼接用户输入(格式化字符串注入攻击)
  • 浮点数精度控制要明确,别用默认格式
  • 大文件频繁fprintf性能很差,建议先拼好字符串再一次性写入

18.4 三组函数的对比与选择

函数 粒度 性能 安全性 适用场景
fgetc/fputc 1字符 协议解析、字符流处理
fgets/fputs 1行 日志、配置、文本处理
fscanf/fprintf 格式化 结构化数据读写(慎用)

我个人在实际项目中的选择策略是这样的:

  • 写文件:能用fprintf就用fprintf,方便。但如果性能敏感,我会用snprintf拼好缓冲区,然后一次fwrite写入。
  • 读文件:90%的情况用fgets读一行,然后用sscanf或自己写解析器。只有数据格式极其稳定、且我完全控制数据源时,才用fscanf。
  • 二进制数据:别用这些函数,用fread/fwrite。

18.5 知识体系总览

下面这张图把文本文件读写的核心逻辑串起来了。你可以看到,三种读写方式各有各的生态位,没有银弹。

文本文件读写三组函数对比 逐字符 fgetc / fputc 逐行 fgets / fputs 格式化 fscanf / fprintf 特性 • 最底层,控制力最强 • 性能最差(频繁系统调用) • 适合协议解析、字符流 特性 • 最常用,平衡性能与易用 • 缓冲区可控,不易爆内存 • 适合日志、配置、文本 特性 • 最方便,但最危险 • 格式不匹配时易出错 • 建议fgets+sscanf替代 实战建议 写文件用 fprintf(方便)| 读文件用 fgets + sscanf(安全) 二进制数据用 fread/fwrite | 性能敏感时用缓冲区批量操作

18.6 避坑总结

嗯,最后再唠叨几句。文本文件读写看似简单,但坑不少。我这些年踩过的雷,列出来给你参考:

  1. EOF判断:fgetc返回值用int接收,别用char。EOF是-1,不是0xFF。
  2. 缓冲区溢出:fgets第二个参数是缓冲区大小,包括'\0'。别写错。
  3. 换行符处理:Windows是\r\n,Linux是\n。fgets在Windows下会保留\r\n,注意跨平台。
  4. fscanf失败恢复:一旦fscanf匹配失败,文件指针停在原地,后续所有读取都废了。建议用fgets+sscanf。
  5. 文件打开模式:文本文件用"r"/"w",别用"rb"/"wb"——后者在Windows下不会做换行符转换。
  6. 错误检查:fopen、fgets、fscanf的返回值一定要检查。别偷懒。

文本文件读写就这些门道。记住一个原则:能用fgets解决的,就别用fscanf;能用fwrite解决的,就别用fprintf。代码写得稳,晚上睡得香。


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