25、错误处理与调试:feof、ferror、clearerr。文件操作中的常见错误与排查。

文件操作,说白了就是跟操作系统打交道。你想想看,磁盘、网络、权限……任何一个环节出问题,你的程序就可能崩掉。我刚开始写C的时候,总觉得文件读写很简单——打开、读写、关闭,三步走嘛。结果呢?一跑起来,要么读到一堆乱码,要么程序直接段错误。

嗯,今天我们就来聊聊文件操作中的错误处理。三个核心函数:feofferrorclearerr。别看它们个头小,用不对的话,坑可不少。

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 知识体系图

下面这张图总结了文件错误处理的核心逻辑:

文件错误处理核心流程 打开文件 fopen fp == NULL? perror 打印错误 开始读写操作 读写函数返回值检查 返回值 == EOF/NULL? 继续读取 feof / ferror 判断 feof → 正常结束 ferror → 错误处理

25.6 避坑指南

最后,分享几个我这些年总结出来的经验:

  1. 每次读写后立即检查返回值。不要等到循环结束才去判断,那样你根本不知道是哪一步出了问题。
  2. 善用 clearerr。如果你需要从错误中恢复(比如重试读取),记得先清除错误标志。我曾经忘了这一步,结果重试时 ferror 一直返回真,白白浪费了半天时间。
  3. 区分 EOF 和错误。有些函数(如 fgetc)在出错和读到末尾时都返回 EOF。这时候必须用 feofferror 来区分。
  4. 不要迷信 feof。它只告诉你「上一次读取是否尝试越过末尾」,而不是「下一次读取是否会失败」。用返回值判断才是正道。

个人习惯:我写文件操作时,会封装一个错误检查宏。每次调用读写函数后,自动检查 ferror 并打印错误信息。这样既省事又不容易漏掉检查。你可以试试看。

文件错误处理,说白了就是「防患于未然」。你永远不知道用户的磁盘什么时候会满,U盘什么时候会被拔掉。多写几行检查代码,少熬几个通宵调试——这笔账,怎么算都划算。


公众号:蓝海资料掘金营,微信 deep3321