6. 字符串读写函数:fgets与fputs函数详解、字符串读写注意事项、行缓冲问题
字符串读写,说白了就是按「行」来操作文件。
你想想看,我们平时处理配置文件、日志文件、文本数据,哪个不是一行一行来的?fgets 和 fputs 就是干这个活的。我个人习惯,只要处理文本文件,首选就是这两个函数——简单、可控、不容易翻车。
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 知识体系图
下面这张图总结了字符串读写函数的核心逻辑和注意事项:
6.6 避坑指南
最后,分享几个我亲身经历的血泪教训:
- 曾经,我用
fgets读一个二进制文件,结果遇到\0字节就停了,数据全乱。后来才明白——字符串函数是为文本设计的,二进制文件请用fread/fwrite。 - 曾经,我在嵌入式设备上用
fputs写日志,程序突然掉电,最后几条日志全丢了。因为行缓冲没刷新。后来我加了fflush,或者每写一条就fclose再fopen(虽然慢,但安全)。 - 曾经,我写了一个日志解析程序,用
fgets读行,缓冲区设了 256 字节。结果线上日志某行超过 300 字节,程序把一行当两行处理,解析全错。后来我改用getline动态分配缓冲区,问题解决。
嗯,字符串读写看似简单,但细节决定成败。记住:fgets 是你的朋友,gets 是你的敌人,行缓冲是你的暗坑。把这些搞清楚了,文本文件处理就稳了。