6、文件结束符与错误处理:feof()、ferror()、clearerr() 函数的使用,以及如何判断文件读写是否成功。
文件操作中,最让人头疼的是什么?
不是读写逻辑写不对,而是——你根本不知道读写到底成功了没有。
我见过太多新手,写文件读到一半就停了,还以为是代码跑飞了。其实呢,是文件指针撞上了 EOF(End of File),但程序没做任何判断。嗯,今天我们就来聊聊这三个函数:feof()、ferror()、clearerr()。说白了,它们就是文件操作的「体检报告」。
6.1 文件结束符(EOF)到底是什么?
EOF 不是一个字符,而是一个状态标志。它藏在文件流的内部结构里。当你读到文件末尾时,系统会悄悄把这个标志置位。
我习惯把文件流想象成一个「带状态的水管」:
- 正常流水 → 读写正常
- 水流干了 → 遇到 EOF
- 水管堵了 → 发生错误
EOF 标志一旦被置位,后续的读操作就会直接返回失败,直到你手动清除它。
重要概念:EOF 不是文件内容的一部分。它只是一个「哨兵」,告诉你后面没东西了。
6.2 feof():检查是否读到文件尾
feof(fp) 的返回值很简单:
- 非零值 → 已经到达文件末尾
- 0 → 还没到末尾
但有个坑,我当年踩过:feof() 只有在 尝试读取越过文件末尾之后 才会返回真。什么意思?
看代码:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (!fp) {
perror("打开文件失败");
return 1;
}
char ch;
// 错误写法:先判断 feof,再读
while (!feof(fp)) {
ch = fgetc(fp);
putchar(ch);
}
fclose(fp);
return 0;
}
这段代码会多输出一个乱码字符。为什么?
因为最后一次 fgetc() 已经读到了 EOF,但 feof() 是在 读操作之后 才被置位的。所以循环会多执行一次,把 EOF 当作字符输出了。
避坑指南:我曾经在一个嵌入式日志解析程序里用了这种写法,结果每次解析最后一行都会多出一个 0xFF。排查了整整一下午才发现是 feof() 的时机问题。
正确的写法应该是:
// 正确写法:先读,再判断
while (1) {
ch = fgetc(fp);
if (feof(fp)) break; // 读到末尾就退出
putchar(ch);
}
或者更简洁的:
while ((ch = fgetc(fp)) != EOF) {
putchar(ch);
}
6.3 ferror():检查读写是否出错
ferror(fp) 检查文件流是否发生了错误。返回非零值表示有错误发生。
错误和 EOF 是两码事:
- EOF → 正常结束,不是错误
- 错误 → 磁盘坏了、权限不够、文件被删了……
我建议你在每次读写操作后都检查一下 ferror(),尤其是在嵌入式环境里。SD 卡偶尔会掉线,NAND Flash 会有坏块,这些都会导致读写失败。
FILE *fp = fopen("data.bin", "rb");
if (!fp) {
perror("打开文件失败");
return -1;
}
size_t bytes = fread(buffer, 1, sizeof(buffer), fp);
if (bytes != sizeof(buffer)) {
if (feof(fp)) {
printf("文件比预期小,只读了 %zu 字节\n", bytes);
}
if (ferror(fp)) {
printf("读取过程中发生错误\n");
perror("具体错误");
}
}
fclose(fp);
个人习惯:我在项目中会写一个宏,把 feof() 和 ferror() 一起检查,省得每次都要写两遍判断。
6.4 clearerr():清除错误和 EOF 标志
有时候,你需要「重置」文件流的状态。比如:
- 读文件时遇到了 EOF,但你想重新从头读
- 发生了可恢复的错误,比如网络闪断后重连
clearerr(fp) 会同时清除 EOF 标志和错误标志。注意,它不会移动文件指针,也不会关闭文件。
FILE *fp = fopen("config.ini", "r");
if (!fp) return;
char line[256];
// 第一次读取
while (fgets(line, sizeof(line), fp)) {
// 处理配置...
}
// 此时 feof(fp) 为真
// 想重新读取?先清除标志
clearerr(fp);
rewind(fp); // 把文件指针移回开头
// 现在可以重新读取了
while (fgets(line, sizeof(line), fp)) {
// 再次处理...
}
fclose(fp);
注意:clearerr() 只清除标志,不修复底层问题。如果错误是硬件层面的(比如磁盘损坏),清除标志后再次读写还是会失败。
6.5 如何判断文件读写是否成功?
我总结了一个「三步检查法」:
- 检查返回值:
fread()、fwrite()、fscanf()等函数都会返回实际读写的数据量。如果小于预期,就要警惕了。 - 区分 EOF 和错误:用
feof()判断是否到文件尾,用ferror()判断是否发生错误。 - 查看具体错误:用
perror()或strerror(errno)打印系统错误信息。
你想想看,如果只检查返回值,你只能知道「读写没完成」,但不知道是正常结束还是出错了。这就是为什么 feof() 和 ferror() 要搭配使用。
6.6 知识体系图
下面这张图帮你理清文件状态检查的完整流程:
6.7 综合示例:一个健壮的文件读取函数
最后,我写一个实际项目中会用到的函数。它把上面所有知识点都串起来了:
#include <stdio.h>
#include <errno.h>
#include <string.h>
/**
* 安全读取文件内容到缓冲区
* 返回值:0 成功,-1 出错,1 文件比预期小(但没出错)
*/
int safe_read_file(const char *path, char *buf, size_t size) {
FILE *fp = fopen(path, "rb");
if (!fp) {
fprintf(stderr, "打开文件 %s 失败: %s\n", path, strerror(errno));
return -1;
}
size_t bytes_read = fread(buf, 1, size, fp);
if (bytes_read == size) {
// 完美,读满了
fclose(fp);
return 0;
}
// 没读满,需要判断原因
if (feof(fp)) {
// 文件比缓冲区小,正常情况
buf[bytes_read] = '\0'; // 加个字符串结束符
printf("文件 %s 只有 %zu 字节\n", path, bytes_read);
fclose(fp);
return 1;
}
if (ferror(fp)) {
// 真的出错了
fprintf(stderr, "读取文件 %s 时出错: %s\n", path, strerror(errno));
clearerr(fp); // 清除错误标志,方便后续操作
fclose(fp);
return -1;
}
// 理论上不会走到这里
fclose(fp);
return 0;
}
我的建议:把这个函数模板保存下来。每次做文件读取时直接套用,能省掉 80% 的调试时间。
好了,关于文件结束符和错误处理,就聊这么多。记住三个函数的名字和用途,写文件操作时多问自己一句:「如果读失败了,我知道为什么吗?」
公众号:蓝海资料掘金营,微信deep3321