3. 文件的顺序读写(一):fgetc()、fputc()、fgets()、fputs() 函数的使用与区别

各位同学,今天我们来聊聊文件操作里最基础、也最常用的一组函数——字符和字符串的读写。

说实话,我刚开始做嵌入式开发那会儿,觉得文件操作不就是读读写写嘛,有啥难的?结果有一次在调试一个数据采集系统时,就因为搞混了 fgets()fputs() 的行为,硬是排查了整整一个下午。嗯,从那以后,我对这几个函数就格外上心了。

3.1 字符级读写:fgetc() 与 fputc()

先看最基础的。这两个函数一次只处理一个字符,简单但实用。

3.1.1 fputc() —— 写一个字符到文件

原型长这样:

int fputc(int ch, FILE *stream);

参数 ch 虽然声明为 int,但你传一个 char 进去完全没问题。返回写入的字符,失败返回 EOF

我个人的习惯是,写完之后立刻检查返回值。别偷懒,很多隐蔽的 bug 都是因为没做这个检查。

核心要点:fputc() 写入成功后,文件位置指针会自动向后移动一个字节。

3.1.2 fgetc() —— 读一个字符

int fgetc(FILE *stream);

返回读取的字符(转为 int),读到文件末尾或出错时返回 EOF

这里有个坑,我踩过。你想想看,char 是有符号还是无符号?如果文件里有个字节是 0xFF,用 char 接收再跟 EOF(通常是 -1)比较,结果会怎样?

注意:千万不要用 char 类型变量接收 fgetc() 的返回值!必须用 int。否则,当读到 0xFF 时,它会被提升为 int 的 -1,跟 EOF 混淆。

我曾经在一个串口日志解析程序里犯过这个错,结果文件里只要出现 0xFF 就提前结束读取。排查了俩小时才发现是类型问题。

3.1.3 实战:逐字符复制文件

#include <stdio.h>

int main() {
    FILE *src = fopen("source.txt", "r");
    FILE *dst = fopen("dest.txt", "w");
    int ch;

    if (src == NULL || dst == NULL) {
        perror("文件打开失败");
        return 1;
    }

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

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

这段代码很经典。注意 while 条件里的写法——先把 fgetc() 的结果赋给 ch,再跟 EOF 比较。这是标准写法,别写成 while (!feof(src)),那个坑更大。

3.2 字符串级读写:fgets() 与 fputs()

字符级读写效率低,一次一个字节。实际项目中,我们更常用字符串级的函数。

3.2.1 fgets() —— 读一行字符串

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

这个函数最多读取 n-1 个字符,遇到换行符 '\n' 或文件末尾就停止。读取的字符串末尾会自动加上 '\0'

注意,fgets() 会保留换行符。这一点跟 gets() 完全不同——gets() 会丢弃换行符,而且不检查缓冲区长度,极其危险。我建议你永远不要用 gets()

小技巧:如果读取的字符串中包含换行符,可以用 str[strcspn(str, "\n")] = '\0'; 把它去掉。这是我常用的手法。

3.2.2 fputs() —— 写一个字符串

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

str 写入文件,不包含末尾的 '\0'。成功返回非负数,失败返回 EOF

fgets() 不同,fputs() 不会自动添加换行符。你需要自己加,比如:

fputs("Hello, world!\n", fp);

3.3 核心区别对比

我把这几个函数的关键区别整理成了一张表,方便你对照:

函数 操作单位 是否保留换行符 缓冲区安全 典型用途
fgetc() 1 字节 逐字节处理,如文件校验
fputc() 1 字节 逐字节写入
fgets() 字符串(行) 保留 是(指定 n) 读取配置文件、日志行
fputs() 字符串 不自动添加 写入文本行

3.4 知识体系总览

下面这张图帮你理清这几个函数的关系和适用场景:

文件顺序读写函数体系 字符级读写 字符串级读写 fputc() fgetc() fputs() fgets() 特性 • 一次操作 1 字节 • 适合二进制文件处理 • 注意 EOF 与 0xFF 的区分 • 效率较低,但控制精细 特性 • 一次操作一行/字符串 • fgets() 保留换行符 • fputs() 不自动加换行 • 适合文本文件处理

3.5 避坑指南与经验之谈

最后,分享几个我这些年积累下来的经验:

  • 缓冲区大小要留余量:用 fgets() 时,n 参数一定要比实际需要的多 1,因为要留位置给 '\0'。我见过有人写 fgets(buf, 100, fp) 然后以为能读 100 个字符,其实最多读 99 个。
  • 混合使用要小心:不要在同一文件流上混用字符级和字符串级函数而不考虑缓冲区状态。标准库内部有缓冲区,混用可能导致数据错乱。
  • 检查返回值是铁律:不管是读还是写,都要检查返回值。我曾经在一个无人值守的采集设备上,因为没检查 fputs() 的返回值,SD 卡写满后程序还在跑,但数据全丢了。
  • 文本模式 vs 二进制模式:在 Windows 上,文本模式下 '\n' 会被转换为 '\r\n'。如果你在处理二进制文件,记得用 "rb""wb" 模式打开。

好了,这一章的内容就到这里。这几个函数虽然基础,但用好了能解决 80% 的文件读写需求。下一章我们继续深入,看看格式化读写和块读写怎么玩。


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