文件结束与错误检测:feof的陷阱、ferror的正确使用、clearerr清除状态
文件操作中,最让人头疼的是什么?我个人觉得,不是读写逻辑写不对,而是你根本不知道文件操作到底成功了没有。很多新手写文件读写,写完就跑,也不检查返回值。结果呢?数据没写进去,或者读到一半就停了,程序还一脸无辜地告诉你「一切正常」。
嗯,今天我们就来聊聊文件结束和错误检测。这三个函数——feof、ferror、clearerr——看着简单,但用起来坑不少。我在项目中就吃过它们的亏,今天一并讲清楚。
1. feof 的陷阱:它不是你想象的那样
先问一个问题:feof(fp) 返回真,代表什么?
很多人脱口而出:「文件读到末尾了呗。」
错!feof 返回真,只代表「上一次读操作因为到达文件末尾而失败」。它不是在预测未来,而是在回顾历史。你想想看,这个区别有多大?
看个经典的反面教材:
FILE *fp = fopen("data.txt", "r");
char buf[256];
while (!feof(fp)) {
fgets(buf, sizeof(buf), fp);
printf("%s", buf);
}
fclose(fp);
这段代码有什么问题?问题大了。假设文件最后一行后面没有换行符,fgets 读到末尾时,实际上已经读到了内容,但 feof 还没变成真。循环继续执行,fgets 再次被调用——这次读不到东西了,返回 NULL,但 buf 里还是上一次的内容。于是你多打印了一行重复数据。
我曾经在一个日志解析工具里就踩过这个坑。解析出来的日志行数总是比实际多一行,排查了半天,最后发现是 feof 用错了地方。说实话,当时真想抽自己一巴掌。
feof 作为循环的结束条件。正确的做法是检查读函数的返回值。比如 fgets 返回 NULL 就结束,fread 返回 0 就结束。
那 feof 到底什么时候用?我个人习惯是在读操作失败之后,用它来区分「到底是读错了,还是读到末尾了」。比如:
if (fgets(buf, sizeof(buf), fp) == NULL) {
if (feof(fp)) {
printf("正常读到文件末尾\n");
} else if (ferror(fp)) {
printf("读文件时发生错误\n");
}
}
这才是 feof 的正确打开方式。说白了,它就是个「事后诸葛亮」,别拿它当「事前预言家」。
2. ferror 的正确使用:别等出事了才想起来
ferror(fp) 用来检查文件流上是否发生了错误。一旦某个读写操作出错,这个标志位就会被置位,并且一直保持,直到你主动清除它。
我见过不少代码,从头到尾都不调用 ferror。程序跑着跑着突然写文件失败了,也不报错,数据就丢了。你说冤不冤?
正确的做法是:每次重要的读写操作之后,都检查一下 ferror。尤其是写文件,因为写失败的原因很多——磁盘满了、权限不够、文件系统损坏等等。
FILE *fp = fopen("output.dat", "wb");
if (!fp) {
perror("fopen failed");
return -1;
}
size_t written = fwrite(data, 1, sizeof(data), fp);
if (written != sizeof(data)) {
if (ferror(fp)) {
fprintf(stderr, "写文件时发生错误\n");
clearerr(fp); // 清除错误状态,后面可能还要继续操作
}
}
fclose(fp);
这里有个细节:fwrite 返回实际写入的字节数。如果它小于你期望的字节数,不一定就是错误,也可能是磁盘满了。这时候 ferror 就能帮你判断到底是哪种情况。
ferror 捕获到错误,然后程序自动切换到备用存储区。如果没有这个检查,数据就全丢了。
3. clearerr 清除状态:让文件流「重获新生」
文件流一旦发生错误或到达末尾,对应的标志位就会一直保持。如果你不主动清除,后续的所有读写操作都会受到影响。
clearerr(fp) 的作用就是同时清除文件结束标志和错误标志。调用之后,文件流恢复「干净」状态,可以继续操作。
什么时候需要清除?我举两个场景:
- 错误恢复后继续读写:比如写文件时磁盘满了,你清理了空间,想继续写。这时候必须先
clearerr,否则写操作会一直失败。 - 读到末尾后重新定位:比如你用
fseek跳回文件开头重新读,但feof标志还置着,不清除的话,fread可能直接返回 0。
FILE *fp = fopen("data.bin", "rb+");
if (!fp) return -1;
// 第一次读取
char buf[64];
fread(buf, 1, 64, fp);
if (feof(fp)) {
printf("第一次读取到末尾\n");
}
// 回到文件开头
rewind(fp); // rewind 内部会调用 clearerr
// 第二次读取
fread(buf, 1, 64, fp);
if (feof(fp)) {
printf("第二次读取到末尾\n");
}
fclose(fp);
注意,rewind 和 fseek 会自动调用 clearerr,所以如果你用它们来重新定位,不需要手动清除。但如果你用其他方式恢复(比如重新打开文件),那就得手动调一下。
feof只告诉你「上次读操作是否因末尾而失败」,别用它控制循环ferror告诉你「文件流上是否发生了错误」,每次重要操作后都检查一下clearerr清除错误和结束标志,让文件流恢复可用状态
4. 知识体系图:文件状态检测的核心逻辑
下面这张图帮你理清这三个函数的关系和使用时机:
这张图的核心逻辑很简单:先执行读写操作,然后检查返回值。如果返回值表明操作失败,再用 feof 和 ferror 来区分原因。最后,如果需要继续操作,用 clearerr 清除状态。
5. 综合示例:一个健壮的文件读取函数
最后,我写一个完整的示例,把今天讲的知识点串起来。这个函数读取一个二进制文件,如果遇到错误会尝试恢复:
#include <stdio.h>
#include <stdlib.h>
int read_file_safely(const char *filename, char *buffer, size_t size) {
FILE *fp = fopen(filename, "rb");
if (!fp) {
perror("fopen");
return -1;
}
size_t total_read = 0;
while (total_read < size) {
size_t ret = fread(buffer + total_read, 1, size - total_read, fp);
if (ret == 0) {
if (feof(fp)) {
// 正常读到末尾,不是错误
printf("文件读取完成,共读取 %zu 字节\n", total_read);
break;
} else if (ferror(fp)) {
// 发生了错误,尝试恢复
fprintf(stderr, "读取错误,尝试恢复...\n");
clearerr(fp);
// 这里可以加一些恢复逻辑,比如重新定位
if (fseek(fp, total_read, SEEK_SET) != 0) {
fprintf(stderr, "恢复失败\n");
fclose(fp);
return -1;
}
continue; // 重试
}
}
total_read += ret;
}
fclose(fp);
return total_read;
}
这个函数里,我同时用到了 feof、ferror 和 clearerr。每次 fread 返回 0 时,先判断是末尾还是错误。如果是错误,清除标志后尝试恢复。你想想看,如果没有这些检查,程序遇到磁盘错误就直接崩溃了,多不靠谱。
公众号:蓝海资料掘金营,微信deep3321