29、文件定位与错误处理:fseek, ftell, rewind, feof, ferror

文件操作,说白了就是跟数据流打交道。你打开文件、读写数据,但很多时候你并不知道自己「读到哪了」。我刚开始做嵌入式项目时,就吃过这个亏——读配置文件读到一半,程序突然崩溃,查了半天才发现是文件指针跑偏了。

嗯,今天我们就来聊聊文件定位和错误处理。这几个函数虽然基础,但用不好真的会出大问题。

文件指针的「当前位置」

每个打开的文件,系统都会维护一个内部指针,叫「文件位置指示器」。你每次读写,这个指针都会自动往后移动。你可以把它想象成书签——你读到哪,书签就夹到哪。

那问题来了:怎么知道当前读到哪了?怎么跳回去重读?

ftell:告诉我你在哪

ftell 的作用很简单——返回当前文件指针相对于文件开头的偏移量(字节数)。

long ftell(FILE *stream);

返回值是 long 类型,如果出错返回 -1L。

典型用法:获取文件大小

FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
    perror("打开文件失败");
    return -1;
}

fseek(fp, 0, SEEK_END);   // 跳到文件末尾
long size = ftell(fp);     // 获取偏移量 = 文件大小
printf("文件大小: %ld 字节\n", size);

fclose(fp);

我在项目中遇到过一个问题:用 ftell 获取文件大小后,忘了把指针复位,结果后续读取全乱了。你想想看,指针在末尾,你读什么?

fseek:想去哪就去哪

fseek 是文件定位的核心函数。它允许你把文件指针移动到任意位置。

int fseek(FILE *stream, long offset, int whence);

三个参数:

  • stream:文件指针
  • offset:偏移量(可正可负)
  • whence:起始位置,有三个宏
含义
SEEK_SET 0 从文件开头算起
SEEK_CUR 1 从当前位置算起
SEEK_END 2 从文件末尾算起

成功返回 0,失败返回非 0。

小技巧:fseek(fp, -10, SEEK_END) 可以跳到文件末尾前 10 个字节的位置。这在读取文件尾部数据时非常实用。

// 读取文件最后 20 个字节
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) return -1;

fseek(fp, -20, SEEK_END);
char buf[21] = {0};
fread(buf, 1, 20, fp);
printf("文件尾部: %s\n", buf);

fclose(fp);

rewind:一键复位

rewind 相当于 fseek(fp, 0, SEEK_SET),但它还会清除文件的错误标志和 EOF 标志。

void rewind(FILE *stream);

没有返回值,也不会告诉你是否失败。我个人习惯用 fseek 代替 rewind,因为 fseek 能返回错误状态。

注意:rewind 不会返回错误。如果文件流已经损坏,你调用 rewind 后可能仍然无法正常读写,但你完全不知道。

feof:真的读到文件尾了吗?

feof 用来检测是否到达文件末尾。但这里有个坑——它不是在读取之前判断,而是在读取之后判断。

int feof(FILE *stream);

到达文件末尾返回非 0,否则返回 0。

我曾经犯过一个经典错误:

// ❌ 错误用法
while (!feof(fp)) {
    fgets(buf, sizeof(buf), fp);
    printf("%s", buf);
}

为什么会这样?因为 feof 只有在读取操作尝试越过文件末尾后才会返回真。上面的循环会多读一次,打印出重复或无效的数据。

正确的做法是:

// ✅ 正确用法
while (fgets(buf, sizeof(buf), fp) != NULL) {
    printf("%s", buf);
}

核心原则:永远不要用 feof 作为循环条件。用读取函数的返回值来判断是否结束。

ferror:出错了别闷着

ferror 用来检查文件流是否发生了错误。一旦出错,这个错误标志会一直保持,直到你调用 clearerrrewind

int ferror(FILE *stream);

有错误返回非 0,否则返回 0。

FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
    perror("打开失败");
    return -1;
}

int ch;
while ((ch = fgetc(fp)) != EOF) {
    // 处理字符
}

if (ferror(fp)) {
    printf("读取过程中发生了错误\n");
    clearerr(fp);  // 清除错误标志
}

fclose(fp);

嗯,这里要注意:EOF 既可能是文件结束,也可能是读取错误。所以读完数据后,一定要用 ferror 确认一下到底是哪种情况。

clearerr:重置状态

当你处理完错误或到达文件末尾后,如果想继续操作这个文件流,需要调用 clearerr 清除错误标志和 EOF 标志。

void clearerr(FILE *stream);

没有返回值。调用后,feofferror 都会返回 0。

知识体系图

下面这张图帮你理清这几个函数的关系:

文件定位与错误处理函数体系 文件流 (FILE*) 定位操作 fseek() ftell() rewind() 错误处理 feof() ferror() clearerr() 定位函数控制「读写位置」 错误函数检测「读写状态」

综合示例:安全读写二进制文件

把上面这些函数串起来,写一个健壮的文件读取函数:

#include <stdio.h>
#include <stdlib.h>

int read_file_safe(const char *filename, unsigned char *buffer, size_t size) {
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL) {
        perror("fopen");
        return -1;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    if (file_size < 0) {
        perror("ftell");
        fclose(fp);
        return -1;
    }

    // 检查缓冲区是否足够
    if ((size_t)file_size > size) {
        fprintf(stderr, "缓冲区太小\n");
        fclose(fp);
        return -1;
    }

    // 回到文件开头
    rewind(fp);

    // 读取数据
    size_t bytes_read = fread(buffer, 1, file_size, fp);
    if (bytes_read != (size_t)file_size) {
        if (ferror(fp)) {
            perror("fread");
        } else if (feof(fp)) {
            fprintf(stderr, "意外到达文件末尾\n");
        }
        fclose(fp);
        return -1;
    }

    fclose(fp);
    return file_size;
}

个人建议:每次调用 freadfwrite 后,都检查一下 ferror。尤其是在嵌入式环境下,SD 卡可能突然拔出,文件系统可能出错。别等到程序崩溃了才去查。

避坑指南

  • 文本模式下的 fseek:在 Windows 上,文本模式会做换行符转换(\n → \r\n),导致 ftell 返回的值不可靠。我建议二进制文件用 "rb""wb" 模式。
  • fseek 的 offset 不要太大:在 32 位系统上,long 最大只有 2GB。超过 2GB 的文件需要用 fseeko64 或平台相关函数。
  • rewind 不报错:如果你需要知道操作是否成功,用 fseek(fp, 0, SEEK_SET) 代替。
  • feof 和 ferror 要配合使用:单独用 feof 判断文件结束是不够的,因为读取错误也会导致循环退出。

好了,文件定位和错误处理就讲到这里。这几个函数看着简单,但用好了能让你的代码健壮很多。下次写文件操作时,记得加上错误检查——别像我当年那样,出了问题才回头补。


专注资料整理