6. 字符串读写函数:fgets与fputs函数详解、字符串读写注意事项、行缓冲问题

字符串读写,说白了就是按「行」来操作文件。

你想想看,我们平时处理配置文件、日志文件、文本数据,哪个不是一行一行来的?fgetsfputs 就是干这个活的。我个人习惯,只要处理文本文件,首选就是这两个函数——简单、可控、不容易翻车。

6.1 fgets:安全的字符串读取

先看原型:

char *fgets(char *s, int size, FILE *stream);

它从 stream 中读取最多 size-1 个字符,遇到换行符或 EOF 就停。读到的内容(包括换行符)存入 s,末尾自动加 \0

嗯,这里要注意:size 是缓冲区大小,不是你想读的字符数。我见过太多新手写 fgets(buf, 256, fp) 然后以为能读 256 个字符——实际上最多读 255 个,第 256 个位置留给 \0

关键行为:

  • 读到换行符会保留它(跟 gets 不一样)
  • 自动加 \0 结尾
  • 如果一行超过 size-1 个字符,只读一部分,剩下的下次再读
  • 成功返回 s,失败或 EOF 返回 NULL

举个例子:

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

int main() {
    FILE *fp = fopen("test.txt", "r");
    if (!fp) {
        perror("fopen");
        return 1;
    }

    char buf[64];
    while (fgets(buf, sizeof(buf), fp)) {
        // 去掉末尾的换行符(如果有)
        size_t len = strlen(buf);
        if (len > 0 && buf[len-1] == '\n') {
            buf[len-1] = '\0';
        }
        printf("读到: %s\n", buf);
    }

    fclose(fp);
    return 0;
}

我在项目中遇到过一个问题:用 fgets 读一个很大的日志文件,每行都超过 4KB。结果缓冲区设小了,一行被拆成多次读取,逻辑全乱套。后来我养成了习惯——先确认文件的最大行长度,再设缓冲区。

警告:永远不要用 gets()!它不检查缓冲区长度,缓冲区溢出是家常便饭。C11 标准已经把它删了。用 fgets 替代,安全第一。

6.2 fputs:字符串写入

原型:

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

把字符串 s 写入 stream,不自动加换行符。成功返回非负数,失败返回 EOF

注意:fputs 写入的是 \0 之前的全部字符,但 \0 本身不写入文件。所以如果你要换行,得自己加 \n

FILE *fp = fopen("output.txt", "w");
if (!fp) return 1;

fputs("第一行\n", fp);  // 手动加换行
fputs("第二行\n", fp);
fputs("第三行", fp);    // 最后一行没有换行

fclose(fp);

我曾经犯过一个低级错误:用 fputs 写配置文件,忘了加换行符。结果所有配置项挤在一行,解析程序直接崩溃。从那以后,我写 fputs 时都会下意识检查末尾有没有 \n

6.3 字符串读写注意事项

这里列几个我踩过的坑:

  • 换行符处理fgets 保留换行符,fputs 不自动加。读写要配对,否则行格式会乱。
  • 缓冲区大小fgets 的第二个参数是缓冲区总大小,不是「想读的字符数」。设小了会截断行。
  • 返回值检查fgets 返回 NULL 可能是 EOF,也可能是错误。用 feof()ferror() 区分。
  • 二进制文件慎用fgets/fputs 是为文本文件设计的。二进制文件里可能有 \0\n 的字节,会导致意外截断。
  • 行长度不确定:如果文件行很长,建议用 getline()(POSIX 扩展)或自己实现动态缓冲区。

小技巧:fgets 读一行后,可以用 strcspn 快速去掉换行符:

buf[strcspn(buf, "\n")] = '\0';

比手动判断更简洁,而且安全。

6.4 行缓冲问题

行缓冲,说白了就是「遇到换行符才真正写入」。标准 I/O 库有三种缓冲模式:

缓冲模式 触发条件 典型场景
全缓冲 缓冲区满才写入 普通文件(默认)
行缓冲 遇到换行符或缓冲区满 终端(stdin/stdout)
无缓冲 立即写入 stderr

为什么会这样?因为效率。全缓冲减少系统调用次数,行缓冲兼顾交互性和性能,无缓冲保证错误信息及时输出。

我遇到过一个问题:程序用 fprintf(stdout, ...) 输出调试信息,但没加 \n,结果信息一直不显示。程序崩溃后,缓冲区内容全丢了。嗯,这就是行缓冲的坑——没遇到换行符,数据就卡在缓冲区里。

解决方法:

  • 输出时加 \n
  • 或者用 fflush(stdout) 强制刷新
  • 或者用 setbuf(stdout, NULL) 设为无缓冲
// 强制刷新示例
fprintf(stdout, "处理中...");
fflush(stdout);  // 立即输出,不等换行符

// 或者直接设无缓冲
setbuf(stdout, NULL);
fprintf(stdout, "处理中...");  // 立即输出

注意:行缓冲只对 stdout 有效。如果你重定向了输出到文件,就变成全缓冲了。调试时记得加 fflush\n,否则可能看不到实时输出。

6.5 知识体系图

下面这张图总结了字符串读写函数的核心逻辑和注意事项:

字符串读写函数知识体系 fgets / fputs fgets 读取 • 最多读 size-1 个字符 • 保留换行符 • 自动加 \0 结尾 • 返回 NULL 表示结束/错误 fputs 写入 • 写入 \0 前所有字符 • 不自动加换行符 • 成功返回非负数 • 失败返回 EOF 行缓冲问题 • 全缓冲:文件默认 • 行缓冲:终端 stdout • 无缓冲:stderr • 没 \n 可能不输出! ⚠ 注意事项 换行符配对 | 缓冲区大小 | 返回值检查 | 二进制文件慎用 | 行长度不确定 ✅ 最佳实践 用 fgets 替代 gets | 手动处理换行符 | 必要时 fflush

6.6 避坑指南

最后,分享几个我亲身经历的血泪教训:

  • 曾经,我用 fgets 读一个二进制文件,结果遇到 \0 字节就停了,数据全乱。后来才明白——字符串函数是为文本设计的,二进制文件请用 fread/fwrite
  • 曾经,我在嵌入式设备上用 fputs 写日志,程序突然掉电,最后几条日志全丢了。因为行缓冲没刷新。后来我加了 fflush,或者每写一条就 fclosefopen(虽然慢,但安全)。
  • 曾经,我写了一个日志解析程序,用 fgets 读行,缓冲区设了 256 字节。结果线上日志某行超过 300 字节,程序把一行当两行处理,解析全错。后来我改用 getline 动态分配缓冲区,问题解决。

嗯,字符串读写看似简单,但细节决定成败。记住:fgets 是你的朋友,gets 是你的敌人,行缓冲是你的暗坑。把这些搞清楚了,文本文件处理就稳了。


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