25、错误处理与调试:feof、ferror、clearerr。文件操作中的常见错误与排查。
文件操作,说白了就是跟操作系统打交道。你想想看,磁盘、网络、权限……任何一个环节出问题,你的程序就可能崩掉。我刚开始写C的时候,总觉得文件读写很简单——打开、读写、关闭,三步走嘛。结果呢?一跑起来,要么读到一堆乱码,要么程序直接段错误。
嗯,今天我们就来聊聊文件操作中的错误处理。三个核心函数:feof、ferror、clearerr。别看它们个头小,用不对的话,坑可不少。
25.1 文件流的状态标志
每个文件流(FILE *)内部都维护着几个状态标志。我个人习惯把它们想象成三个小旗子:
- EOF标志:读到文件末尾时,这个旗子被立起来。
- 错误标志:读写过程中发生错误时,这个旗子被立起来。
- 正常状态:两个旗子都没立,一切安好。
这三个函数就是用来操作这些旗子的:
| 函数 | 作用 | 返回值 |
|---|---|---|
feof(fp) |
检查EOF标志是否被设置 | 非0表示已到文件末尾 |
ferror(fp) |
检查错误标志是否被设置 | 非0表示发生了错误 |
clearerr(fp) |
清除EOF和错误标志 | 无返回值 |
关键点:feof 并不是在读取到EOF字符时立即返回真。它是在上一次读取操作尝试越过文件末尾之后才返回真。这个区别,曾经让我调试了整整一个下午。
25.2 feof 的正确用法
很多初学者会这样写:
// ❌ 错误示范
while (!feof(fp)) {
fgets(buffer, sizeof(buffer), fp);
printf("%s", buffer);
}
为什么错了?因为 feof 是在读取操作之后才判断的。当最后一次 fgets 读到文件末尾时,它返回NULL,但此时 feof 还没返回真。循环继续执行,fgets 再次尝试读取——失败,buffer内容不变,然后你打印了重复的最后一行。
正确的写法应该是:
// ✅ 正确示范
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
printf("%s", buffer);
}
// 循环结束后,再用 feof 判断是否正常结束
if (feof(fp)) {
printf("正常读取完毕\n");
} else if (ferror(fp)) {
printf("读取过程中发生错误\n");
}
我在项目中遇到过类似的问题。有一次处理一个几十GB的日志文件,用 while (!feof) 的方式读,结果最后一行被重复处理了两次,导致统计结果多了一倍。嗯,从那以后我再也不敢把 feof 放在循环条件里了。
25.3 ferror 与 clearerr 的配合
ferror 用来检查文件流是否发生了错误。一旦错误标志被设置,后续的所有读写操作都会失败,直到你调用 clearerr 清除它。
来看一个实际场景:
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("打开文件失败");
return -1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 处理字符
putchar(ch);
}
// 检查是正常结束还是出错
if (ferror(fp)) {
printf("读取过程中发生错误!\n");
clearerr(fp); // 清除错误标志,以便后续操作
// 可以尝试重新定位或重试
fseek(fp, 0, SEEK_SET);
} else if (feof(fp)) {
printf("文件读取完毕\n");
}
fclose(fp);
小技巧:perror 函数会打印出最近一次系统错误的描述信息。配合 ferror 使用,能快速定位问题原因。我个人习惯在每次文件操作失败后都调用 perror,省得猜来猜去。
25.4 常见错误与排查方法
文件操作中,我踩过的坑大概可以归纳为这几类:
25.4.1 文件打开失败
- 路径不对:相对路径、绝对路径搞混。我曾经在Linux下用Windows风格的反斜杠,结果文件死活打不开。
- 权限不足:只读模式打开一个只写文件,或者没有读取权限。
- 文件不存在:用
"r"模式打开不存在的文件。
排查方法:打开后立即检查 fp == NULL,并用 perror 打印错误信息。
25.4.2 读写过程中断
- 磁盘空间不足:写入时突然没空间了。
- 设备移除:U盘在读写过程中被拔掉。
- 网络断开:如果是网络文件系统(NFS、SMB),网络中断会导致读写失败。
排查方法:每次读写后检查 ferror,必要时用 clearerr 重置状态后重试。
25.4.3 缓冲区问题
- 未刷新缓冲区:写入后没有
fflush,数据还留在内存里。程序崩溃时数据就丢了。 - 混合读写:在文本模式和二进制模式之间切换,没有正确处理换行符。
警告:千万不要在 fclose 之后还去操作文件指针。我曾经见过有人关闭文件后还调用 feof,结果程序直接崩溃。关闭后的 FILE * 指针是悬空指针,必须置为NULL。
25.5 知识体系图
下面这张图总结了文件错误处理的核心逻辑:
25.6 避坑指南
最后,分享几个我这些年总结出来的经验:
- 每次读写后立即检查返回值。不要等到循环结束才去判断,那样你根本不知道是哪一步出了问题。
- 善用
clearerr。如果你需要从错误中恢复(比如重试读取),记得先清除错误标志。我曾经忘了这一步,结果重试时ferror一直返回真,白白浪费了半天时间。 - 区分
EOF和错误。有些函数(如fgetc)在出错和读到末尾时都返回EOF。这时候必须用feof和ferror来区分。 - 不要迷信
feof。它只告诉你「上一次读取是否尝试越过末尾」,而不是「下一次读取是否会失败」。用返回值判断才是正道。
个人习惯:我写文件操作时,会封装一个错误检查宏。每次调用读写函数后,自动检查 ferror 并打印错误信息。这样既省事又不容易漏掉检查。你可以试试看。
文件错误处理,说白了就是「防患于未然」。你永远不知道用户的磁盘什么时候会满,U盘什么时候会被拔掉。多写几行检查代码,少熬几个通宵调试——这笔账,怎么算都划算。
公众号:蓝海资料掘金营,微信 deep3321