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 如何判断文件读写是否成功?

我总结了一个「三步检查法」:

  1. 检查返回值fread()fwrite()fscanf() 等函数都会返回实际读写的数据量。如果小于预期,就要警惕了。
  2. 区分 EOF 和错误:用 feof() 判断是否到文件尾,用 ferror() 判断是否发生错误。
  3. 查看具体错误:用 perror()strerror(errno) 打印系统错误信息。

你想想看,如果只检查返回值,你只能知道「读写没完成」,但不知道是正常结束还是出错了。这就是为什么 feof()ferror() 要搭配使用。

6.6 知识体系图

下面这张图帮你理清文件状态检查的完整流程:

文件读写状态检查流程 执行读写操作 检查返回值 == 预期大小? 读写成功 ✅ 读写未完成 ❌ feof() 检查 是 → 正常结束 文件尾,正常 ✅ ferror() 检查 错误,需处理 ❌ clearerr() 可清除所有标志,但不会修复底层问题

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