9. 文件定位函数:ftell、fseek、rewind 详解

文件操作里,读写位置的控制是个基本功。说白了,文件就像一盘磁带,你总得知道当前播到哪了,还得能快进、快退、回到开头。C 语言给了我们三个函数干这事:ftellfseekrewind。今天我就把这三个家伙掰开揉碎讲清楚。

9.1 文件位置指示器是什么?

每个打开的文件,系统内部都维护着一个「位置指示器」。它指向文件中的当前字节偏移量。你读一个字节,它就往后挪一个字节;你写一个字节,它也往后挪。嗯,就是这么个东西。

我刚开始做嵌入式开发时,总以为文件读写是「随机访问」的,后来调试一个日志系统才发现——位置指示器没管理好,数据全写串了。从那以后,我对这三个函数就格外上心。

9.2 ftell:告诉我你在哪

ftell 的作用很简单:返回当前文件位置指示器的值,单位是字节。原型如下:

long ftell(FILE *stream);

成功时返回当前偏移量,失败返回 -1。注意,它返回的是 long 类型,对于超大文件可能不够用。不过嵌入式场景下,大部分文件不会超过 2GB,够用了。

核心用法:获取文件大小、记录断点、判断是否到文件尾。

举个例子,获取文件大小:

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

fseek(fp, 0, SEEK_END);   // 跳到文件末尾
long size = ftell(fp);     // 此时 ftell 就是文件大小
printf("文件大小: %ld 字节\n", size);

fclose(fp);

我在项目中遇到过一个问题:用 ftell 获取大小后,忘了把位置调回文件头,结果后续读取全读到了空数据。所以记住——ftell 只是告诉你位置,它不会帮你复位。

9.3 fseek:想去哪就去哪

fseek 是文件定位的核心。它允许你基于某个参考点,移动指定偏移量。原型:

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

参数 whence 有三个取值,我分别说说。

9.3.1 SEEK_SET:从文件头开始算

偏移量从文件开头计算。比如 fseek(fp, 100, SEEK_SET) 就是跳到第 100 个字节处。这是最直观的用法。

// 跳到文件第 64 字节处(比如读取某个结构体)
fseek(fp, 64, SEEK_SET);
fread(&header, sizeof(header), 1, fp);

9.3.2 SEEK_CUR:从当前位置开始算

偏移量基于当前文件位置。正数往后跳,负数往前跳。比如:

// 跳过当前记录,前进 20 字节
fseek(fp, 20, SEEK_CUR);

// 后退 10 字节,重新读取刚才的数据
fseek(fp, -10, SEEK_CUR);

我建议你在使用 SEEK_CUR 时,先调用 ftell 确认当前位置,否则容易算错。我曾经在一个协议解析模块里,因为 SEEK_CUR 的偏移量算反了,导致解析出来的数据全是乱码。调试了整整一个下午才找到原因。

9.3.3 SEEK_END:从文件尾开始算

偏移量从文件末尾计算。注意,SEEK_END 的偏移量通常是负数,因为你要往回跳。比如:

// 跳到文件最后 32 字节
fseek(fp, -32, SEEK_END);

// 跳到文件末尾(准备追加写入)
fseek(fp, 0, SEEK_END);

注意:不是所有流都支持 SEEK_END。比如标准输入 stdin、管道、串口等设备文件,调用 fseek 会失败。嵌入式开发中,操作 UART 或 SPI 设备时千万别用 fseek

9.4 rewind:一键回到起点

rewind 相当于 fseek(fp, 0, SEEK_SET),但它还会清除文件结束标志和错误标志。原型:

void rewind(FILE *stream);

没有返回值,也不会告诉你是否失败。我个人习惯用 fseek 代替 rewind,因为 fseek 能返回错误码,方便排查问题。

// 两种写法等价,但我推荐下面那种
rewind(fp);

// 更安全的写法
if (fseek(fp, 0, SEEK_SET) != 0) {
    perror("fseek failed");
}

9.5 三个函数的对比

函数 功能 返回值 典型场景
ftell 获取当前位置 成功返回偏移量,失败返回 -1 获取文件大小、记录断点
fseek 移动位置 成功返回 0,失败返回非 0 随机读写、跳过数据
rewind 回到文件头 无返回值 重新读取文件

9.6 避坑指南

我总结了几条实战中容易踩的坑,你记一下:

  • 文本模式下的偏移量不可靠:Windows 下文本模式会做换行符转换(\r\n\n),导致 ftell 返回的值和实际字节数对不上。二进制文件请用 "rb""wb" 模式。
  • 不要对非文件流使用 fseek:比如 stdinstdout、管道、socket。这些流不支持定位,调用会失败。
  • fseek 后记得检查返回值:我曾经在一个 SD 卡读写程序里,没检查 fseek 返回值,结果卡没插好,定位失败,后续读写全乱套了。
  • ftell 返回 long,大文件可能溢出:如果文件超过 2GB,ftell 会返回负数。嵌入式场景一般遇不到,但心里要有数。

9.7 知识体系图

下面这张图把文件定位的核心逻辑串起来了,你一看就明白:

文件定位函数知识体系 文件位置指示器 ftell() fseek() rewind() 获取当前位置 • 返回 long 型偏移量 • 失败返回 -1 • 常用于获取文件大小 移动位置指示器 • SEEK_SET / SEEK_CUR / SEEK_END • 成功返回 0 • 支持随机读写 回到文件开头 • 等价于 fseek(fp,0,SEEK_SET) • 清除错误标志 • 无返回值 注意:文本模式下 ftell 返回值可能不准确,建议使用二进制模式

9.8 实战小技巧

我的习惯:每次调用 fseek 后,紧接着调用 ftell 确认位置是否正确。虽然多了一行代码,但能省去很多调试时间。

举个例子,读取一个自定义格式的二进制文件:

// 假设文件结构:文件头(32字节) + 记录1 + 记录2 + ...
FILE *fp = fopen("records.dat", "rb");
if (!fp) return -1;

// 跳过文件头,读取第 3 条记录(每条记录 64 字节)
int record_index = 2;  // 从 0 开始计数
long offset = 32 + record_index * 64;

if (fseek(fp, offset, SEEK_SET) != 0) {
    perror("fseek failed");
    fclose(fp);
    return -1;
}

Record rec;
if (fread(&rec, sizeof(rec), 1, fp) != 1) {
    perror("fread failed");
    fclose(fp);
    return -1;
}

printf("读取记录 %d 成功\n", record_index);
fclose(fp);

你看,先算偏移量,再定位,最后读取。每一步都检查返回值,这样写出来的代码才可靠。

好了,文件定位这块就讲这么多。记住三个函数、三个 whence 常量,再加上我提到的那些坑,你基本就能驾驭文件随机读写了。


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