4. 文件读写位置:ftell与fseek、rewind、feof与ferror、文件指针的移动与随机读写

文件操作中,读写位置是个容易被忽视但极其重要的概念。说白了,文件就像一盘磁带,你读到哪里、写到哪里,全靠一个「位置指针」来标记。我刚开始学C语言时,总觉得文件读写就是从头到尾顺序来一遍,直到有一次做日志分析程序,需要在几百MB的文件里来回跳着读数据,才真正体会到位置控制的重要性。

4.1 文件位置指针的概念

每个打开的文件,系统都会维护一个内部指针,指向当前要读写的字节位置。你调用 freadfwrite 时,数据从这个位置开始操作,操作完成后指针自动后移。这个机制是透明的,但你可以通过几个函数来主动控制它。

核心理解:文件位置指针不是C语言里的指针变量,而是操作系统为每个打开文件维护的一个偏移量(offset),单位是字节。从文件开头算起,0表示文件起始位置。

4.2 ftell:获取当前位置

ftell 函数返回当前文件位置指针相对于文件开头的偏移量。原型很简单:

long ftell(FILE *stream);

返回 -1L 表示出错。我习惯在每次需要知道当前位置时调用它,尤其是在调试文件读写逻辑时。

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

// 读取前100个字节
char buffer[100];
fread(buffer, 1, 100, fp);

long pos = ftell(fp);
printf("当前读取位置:%ld\n", pos);  // 应该输出100

fclose(fp);

嗯,这里要注意:ftell 返回的是 long 类型,对于超过2GB的文件可能会溢出。在64位系统上,可以考虑用 fgetpos 替代,它使用 fpos_t 类型,能支持更大的文件。

4.3 fseek:任意跳转位置

fseek 是文件随机读写的核心函数。它允许你把位置指针设置到文件的任意位置:

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

whence 参数有三个选项:

常量 含义
SEEK_SET 0 从文件开头算起
SEEK_CUR 1 从当前位置算起
SEEK_END 2 从文件末尾算起

举个例子,假设你要读取一个二进制文件的第500到第599字节:

FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) return -1;

// 跳到第500字节
fseek(fp, 500, SEEK_SET);

char buffer[100];
size_t n = fread(buffer, 1, 100, fp);
if (n == 100) {
    printf("成功读取100个字节\n");
}

fclose(fp);

我曾经在项目中遇到一个坑:用 fseek(fp, -10, SEEK_END) 想跳到文件末尾前10个字节,结果文件是空的,fseek 返回了非0值表示失败。所以,调用 fseek 后一定要检查返回值。

警告:在文本模式下("r"、"w"等),fseek 的行为可能因平台而异。Windows下换行符 \r\n 会被转换为 \n,导致位置计算不准确。建议二进制文件用 "rb"、"wb" 模式,确保位置可控。

4.4 rewind:回到起点

rewind 相当于 fseek(fp, 0, SEEK_SET),但更简洁。它把位置指针重置到文件开头,同时清除文件结束标志和错误标志。

void rewind(FILE *stream);

我经常在需要重新读取文件时用它:

FILE *fp = fopen("config.ini", "r");
if (fp == NULL) return -1;

// 第一次读取,统计行数
int lines = 0;
char line[256];
while (fgets(line, sizeof(line), fp)) {
    lines++;
}

// 回到开头,重新读取内容
rewind(fp);

// 第二次读取,处理每一行
while (fgets(line, sizeof(line), fp)) {
    process_line(line);
}

fclose(fp);

注意 rewind 没有返回值,你无法知道它是否失败。如果你需要错误检查,还是用 fseek 吧。

4.5 feof 与 ferror:检查状态

文件操作中,判断「是否读到末尾」和「是否出错」是两个常见需求。feofferror 就是干这个的。

int feof(FILE *stream);
int ferror(FILE *stream);

两者都返回非0值表示「是」,返回0表示「否」。

很多人会犯一个错误:用 feof 来判断文件是否结束,然后才去读数据。实际上,feof 只有在读取操作失败后才会被设置。正确的做法是先读,再判断:

FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) return -1;

char buffer[256];
size_t n;

// 正确做法:先读,再判断
while ((n = fread(buffer, 1, sizeof(buffer), fp)) > 0) {
    // 处理读取到的 n 个字节
    process_data(buffer, n);
}

if (feof(fp)) {
    printf("正常读取到文件末尾\n");
}
if (ferror(fp)) {
    printf("读取过程中发生错误\n");
}

fclose(fp);

我曾经见过这样的代码:

// 错误示范
while (!feof(fp)) {
    fread(buffer, 1, sizeof(buffer), fp);
    // 这里会多处理一次无效数据
}

为什么是错的?因为 feof 在最后一次读取失败后才返回真,但循环体已经执行了一次无效的读取。你想想看,这会导致最后一批数据被重复处理,或者读到垃圾数据。

个人经验:我习惯在每次读取操作后立即检查返回值,而不是依赖 feofferror。这两个函数更多用于区分「正常结束」和「出错结束」。比如,fread 返回0,可能是文件末尾,也可能是读取错误,这时用 feofferror 就能区分。

4.6 随机读写实战:读取结构体数组的任意元素

假设你有一个二进制文件,里面存储了1000个 struct Student 结构体。你想直接读取第500个学生,而不需要遍历前499个。这就是随机读写的典型场景。

#include <stdio.h>

typedef struct {
    int id;
    char name[32];
    float score;
} Student;

int main() {
    FILE *fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long file_size = ftell(fp);
    long record_size = sizeof(Student);
    long record_count = file_size / record_size;

    printf("文件中共有 %ld 条记录\n", record_count);

    // 读取第500个学生(索引从0开始)
    int target_index = 499;
    if (target_index >= record_count) {
        printf("索引超出范围\n");
        fclose(fp);
        return 1;
    }

    // 跳到目标位置
    fseek(fp, target_index * record_size, SEEK_SET);

    Student stu;
    size_t n = fread(&stu, sizeof(Student), 1, fp);
    if (n == 1) {
        printf("第%d个学生:\n", target_index + 1);
        printf("  学号:%d\n", stu.id);
        printf("  姓名:%s\n", stu.name);
        printf("  成绩:%.2f\n", stu.score);
    } else {
        if (feof(fp)) {
            printf("意外到达文件末尾\n");
        }
        if (ferror(fp)) {
            printf("读取错误\n");
        }
    }

    fclose(fp);
    return 0;
}

这个例子展示了随机读写的完整流程:先定位,再读取。定位时用 fseek 计算偏移量,读取后检查 feofferror 来确认状态。

4.7 知识体系图

下面这张图总结了文件位置控制的核心函数和它们之间的关系:

文件位置控制核心函数 文件位置指针 获取当前位置 ftell() 设置任意位置 fseek() / rewind() 检查文件结束 feof() 检查读写错误 ferror() 使用流程: 1. 打开文件 → 获取文件指针 2. 用 fseek() 定位到目标位置 3. 执行读写操作(fread/fwrite) 4. 用 feof()/ferror() 检查操作结果

4.8 避坑指南

最后,分享几个我踩过的坑:

  • 不要混合使用文本模式和二进制模式:在Windows上,文本模式会做换行符转换,导致 ftell 返回的位置和实际字节数不一致。处理二进制数据时,始终用 "rb""wb"
  • fseek 到文件末尾后,不要直接写数据:如果你用 fseek(fp, 0, SEEK_END) 定位到末尾,然后调用 fwrite,数据会追加到文件末尾。但如果你先读后写,位置指针会移动,要小心。
  • rewind 会清除错误标志:如果你在读取过程中发生了错误,调用 rewind 后错误标志会被清除。这有时是好事,有时会掩盖问题。我一般先检查 ferror,再决定是否 rewind
  • feof 不是预测函数:它不会告诉你「下一次读取是否会到达末尾」,而是告诉你「上一次读取是否因为到达末尾而失败」。这个区别很重要。

文件位置控制是C语言文件IO的精髓之一。掌握了 ftellfseekrewindfeofferror,你就能像操作数组一样灵活地读写文件。说白了,就是把文件当成一个巨大的字节数组,用位置指针当下标,想读哪里读哪里。


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