字符读写:fgetc、fputc 与 EOF 的真相
文件操作里,最基础也最容易被忽视的,就是字符级别的读写。很多人一上来就搞 fread、fwrite,结果遇到文本文件里的换行符、乱码问题,回头才发现——连最基础的 fgetc 都没吃透。
我个人习惯,凡是处理文本文件,第一反应就是用 fgetc 和 fputc。为什么?因为字符流是最可控的,你一个字节一个字节地读,出问题能精确定位到第几个字符。今天我们就把这几个函数掰开揉碎了讲清楚。
fputc:写一个字符到文件
先看写操作。fputc 的原型很简单:
int fputc(int ch, FILE *stream);
它把 ch 这个字符(注意是 int 类型)写入 stream 指向的文件。成功时返回写入的字符,失败返回 EOF。
为什么参数是 int 而不是 char?嗯,这里有个历史原因——为了能容纳 EOF 这个特殊值。EOF 在 stdio.h 里通常定义为 -1,而 char 可能是 unsigned 的,用 int 就避免了歧义。
核心要点:fputc 每次只写一个字节。如果你要写字符串,别用它循环——直接用 fputs 或 fprintf 更高效。
看个例子:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
char str[] = "Hello, 嵌入式!";
int i = 0;
while (str[i] != '\0') {
fputc(str[i], fp);
i++;
}
fclose(fp);
return 0;
}
这段代码把字符串逐个字符写入文件。我在项目中遇到过一个问题:用 fputc 写二进制数据时,遇到 0xFF 就出错了。后来才发现,fputc 本质上是为文本流设计的,写二进制数据最好用 fwrite。
fgetc:读一个字符
读操作对应的是 fgetc:
int fgetc(FILE *stream);
它从 stream 中读取下一个字符,返回该字符的 int 值。读到文件末尾或出错时,返回 EOF。
你想想看,如果文件里正好有一个字节是 0xFF(即 -1 的 unsigned 形式),那它和 EOF 怎么区分?这就是为什么 fgetc 返回 int 而不是 char——它把字符值提升到 int 范围,EOF 是 -1,而 0xFF 作为 int 是 255,两者不冲突。
小技巧:用 fgetc 读文件时,一定要把返回值赋给 int 变量,而不是 char。否则 0xFF 会被截断成 -1,和 EOF 混淆。
一个典型的读文件循环:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
int ch; // 注意是 int,不是 char
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 输出到屏幕
}
fclose(fp);
return 0;
}
这个模式你会在无数代码里看到。但我要提醒你:这个循环假设文件是文本文件,且没有二进制数据。如果文件里有 0xFF,循环就会提前终止——因为 fgetc 返回了 0xFF,而它被提升为 int 后是 255,不等于 -1,所以不会提前退出。但如果你用 char 接收,0xFF 被截断成 -1,循环就提前结束了。
警告:永远不要用 char 变量接收 fgetc 的返回值!这是 C 语言初学者最容易犯的错误之一。我曾经调试过一个串口通信程序,对方发来的数据里包含 0xFF,结果程序读到那里就停了,排查了半天才发现是 char 类型的问题。
EOF 的判定:没那么简单
EOF 是 End Of File 的缩写,在 stdio.h 中定义为 -1。但文件末尾和错误都会导致 fgetc 返回 EOF,怎么区分?
这就是 feof 和 ferror 的用武之地了。
| 函数 | 作用 | 返回值 |
|---|---|---|
| feof(fp) | 检查文件是否到达末尾 | 到达末尾返回非0,否则返回0 |
| ferror(fp) | 检查文件是否发生错误 | 发生错误返回非0,否则返回0 |
正确的做法是:先判断 fgetc 是否返回 EOF,如果是,再用 feof 和 ferror 判断具体原因。
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
int ch;
while (1) {
ch = fgetc(fp);
if (ch == EOF) {
if (feof(fp)) {
printf("正常到达文件末尾\n");
} else if (ferror(fp)) {
printf("读取过程中发生错误\n");
perror("错误详情");
}
break;
}
putchar(ch);
}
fclose(fp);
return 0;
}
这个写法虽然啰嗦,但很安全。我见过太多人直接用 while(!feof(fp)) 来循环,结果多读了一次——因为 feof 在读取到末尾之后才会被设置,而不是之前。
避坑指南:不要用 while(!feof(fp)) 作为循环条件!正确的做法是:先读,再判断返回值,最后用 feof/ferror 查原因。
知识体系图
下面这张图把字符读写相关的函数和判定逻辑串起来了:
feof 和 ferror 的细节
这两个函数其实很简单,但有几个坑要注意:
- feof 不会自动清除:一旦文件末尾标志被设置,它会一直保持,直到你调用 clearerr 或 rewind。所以如果你在循环中用了 feof,记得在适当位置清除它。
- ferror 同样需要手动清除:错误标志一旦设置,后续所有操作都会失败,直到你调用 clearerr。
- clearerr 函数:同时清除 feof 和 ferror 的标志。原型是 void clearerr(FILE *stream);
我曾经在一个日志系统里遇到过这个问题:程序运行几天后突然不写日志了,排查发现是某个文件操作出错后,ferror 标志一直没清除,导致后续所有 fputc 都直接返回 EOF。从那以后,我每次在错误处理分支里都会加一句 clearerr。
综合示例:文件复制
把上面这些知识点串起来,写一个文件复制程序:
#include <stdio.h>
int main() {
FILE *src = fopen("source.txt", "r");
FILE *dst = fopen("dest.txt", "w");
if (src == NULL || dst == NULL) {
perror("打开文件失败");
return 1;
}
int ch;
int count = 0;
while ((ch = fgetc(src)) != EOF) {
if (fputc(ch, dst) == EOF) {
if (ferror(dst)) {
fprintf(stderr, "写入错误,已复制 %d 个字符\n", count);
clearerr(dst);
break;
}
}
count++;
}
if (feof(src)) {
printf("文件复制完成,共 %d 个字符\n", count);
} else if (ferror(src)) {
fprintf(stderr, "读取错误\n");
clearerr(src);
}
fclose(src);
fclose(dst);
return 0;
}
这个程序里,我同时检查了读和写的错误。实际项目中,你还要考虑磁盘空间不足、权限问题等。但核心逻辑就是:读一个,写一个,出错就停,并报告进度。
个人建议:在嵌入式系统里,文件操作往往伴随着 SD 卡、Flash 等存储介质。这些介质的读写速度差异很大,而且容易受到电源波动的影响。我建议你在每次 fputc 后都检查返回值,不要偷懒。虽然会多几行代码,但能避免很多诡异的 bug。
好了,字符读写就讲到这里。记住三个关键点:fgetc 返回 int、EOF 不等于文件末尾、feof 和 ferror 要配合使用。把这些基础打牢,后面学 fread/fwrite 时就会轻松很多。