5. 文件的随机读写:fseek()、ftell()、rewind() 函数详解,以及文件内部指针的概念

大家好,我是你们的老朋友。今天我们来聊聊文件操作里一个非常实用的技能——随机读写。

说实话,大部分时候我们读写文件都是顺序的:从头读到尾,或者从头写到尾。但实际项目中,你总会遇到需要“跳着读”的情况。比如,你有一个日志文件,只想看最后100条;或者你有一个配置文件,只想修改其中某个字段。这时候,顺序读写就太笨拙了。

嗯,C语言为我们准备了三个好用的工具:fseek()ftell()rewind()。它们能让我们像操作数组一样,随意定位文件中的任意位置。

5.1 文件内部指针:一个看不见的“光标”

先理解一个核心概念——文件内部指针

你想想看,当你打开一个文件时,系统会为这个文件流维护一个位置指示器。它就像一个光标,指向当前要读写的位置。每次你调用 fread()fwrite(),这个指针就会自动向后移动。

说白了,顺序读写就是让这个指针从头走到尾。而随机读写,就是我们可以手动把这个指针拨到任意位置。

核心理解:文件内部指针不是C语言特有的概念,它是操作系统层面的东西。每个打开的文件描述符都关联着一个文件偏移量(file offset)。fseek() 本质上就是在修改这个偏移量。

我在项目中遇到过一个问题:一个嵌入式设备需要频繁读写SD卡上的配置文件。如果每次都从头读到尾,效率极低。后来我用 fseek() 直接定位到配置项所在的位置,读写速度提升了近10倍。嗯,这就是随机读写的价值。

5.2 fseek():精准定位

fseek() 是随机读写的核心函数。它的原型很简单:

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

三个参数的含义:

  • stream:文件指针,你打开的那个文件流
  • offset:偏移量,单位是字节。正数向后移,负数向前移
  • whence:起始位置,有三个可选值
whence 常量 数值 含义
SEEK_SET 0 从文件开头算起
SEEK_CUR 1 从当前位置算起
SEEK_END 2 从文件末尾算起

举个例子,假设文件里有100个字节:

// 跳到文件开头
fseek(fp, 0, SEEK_SET);

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

// 从当前位置向后跳10个字节
fseek(fp, 10, SEEK_CUR);

// 跳到文件末尾前20个字节
fseek(fp, -20, SEEK_END);

我的小技巧:我习惯用 SEEK_SET 配合绝对偏移量,这样逻辑最清晰。只有在需要相对移动时才用 SEEK_CUR。至于 SEEK_END,我通常只用来获取文件大小。

5.3 ftell():看看你在哪

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

long ftell(FILE *stream);

返回 -1L 表示出错。

这个函数最常见的用途有两个:

  • 获取文件大小:先 fseek(fp, 0, SEEK_END),再 ftell(fp)
  • 保存当前位置:在跳转前记录位置,方便后续恢复

我曾经在调试一个数据采集程序时,发现文件读写总是错位。后来用 ftell() 在每个读写操作前后打印位置,才定位到问题——原来有个地方忘记考虑结构体对齐了。嗯,ftell() 是调试文件操作的好帮手。

5.4 rewind():一键复位

rewind()fseek(fp, 0, SEEK_SET) 的简化版。它把文件内部指针重置到文件开头,同时清除文件结束标志和错误标志。

void rewind(FILE *stream);

注意,它没有返回值。如果你需要检查是否成功,还是用 fseek() 吧。

注意:rewind() 会清除错误标志。如果你之前遇到了文件错误,调用 rewind() 后错误信息就丢了。我建议在需要重置位置时优先用 fseek(),除非你明确知道要清除错误状态。

5.5 实战:一个完整的随机读写示例

光说不练假把式。我们写一个完整的例子:创建一个包含10个整数的文件,然后随机读取第3个和第7个元素。

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

int main() {
    FILE *fp;
    int data[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int value;
    long pos;

    // 写入数据
    fp = fopen("numbers.dat", "wb");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }
    fwrite(data, sizeof(int), 10, fp);
    fclose(fp);

    // 随机读取
    fp = fopen("numbers.dat", "rb");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 读取第3个元素(索引2)
    fseek(fp, 2 * sizeof(int), SEEK_SET);
    fread(&value, sizeof(int), 1, fp);
    printf("第3个元素: %d\n", value);

    // 读取第7个元素(索引6)
    fseek(fp, 6 * sizeof(int), SEEK_SET);
    fread(&value, sizeof(int), 1, fp);
    printf("第7个元素: %d\n", value);

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    pos = ftell(fp);
    printf("文件大小: %ld 字节\n", pos);

    fclose(fp);
    return 0;
}

输出结果:

第3个元素: 30
第7个元素: 70
文件大小: 40 字节

你看,我们跳过了不需要的元素,直接定位到目标位置。这就是随机读写的魅力。

5.6 知识体系:随机读写的核心逻辑

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

文件随机读写核心逻辑 文件流 (FILE *fp) 文件内部指针 fseek() 设置指针位置 ftell() 获取指针位置 rewind() 重置到开头 三者配合,实现文件的精确定位与随机访问

5.7 避坑指南

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

  • 文本模式下的偏移量:在Windows上,文本模式下换行符 \n 会被转换为 \r\n。这时候用 fseek() 计算偏移量可能会出错。我建议二进制文件用二进制模式打开,文本文件用文本模式,别混用。
  • 不要对 stdout/stdin 用 fseek():标准输入输出流不一定支持随机定位。我曾经试图对 stdinfseek(),结果程序直接崩溃。
  • fseek() 的返回值一定要检查:返回0表示成功,非0表示失败。别偷懒不检查,否则定位失败了你都不知道。
  • 大文件注意 long 的范围:在32位系统上,long 最大只能表示2GB。处理大文件时要用 fseeko()ftello(),它们用 off_t 类型,支持更大的文件。

我的习惯:每次调用 fseek() 后,我都会紧跟着调用 ftell() 确认位置是否正确。虽然多了一行代码,但能避免很多隐蔽的bug。

好了,关于随机读写就讲这么多。记住,fseek() 是定位工具,ftell() 是测量工具,rewind() 是复位工具。三者配合,你就能像操作数组一样灵活地操作文件了。


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