9. 文件定位函数:ftell、fseek、rewind 详解
文件操作里,读写位置的控制是个基本功。说白了,文件就像一盘磁带,你总得知道当前播到哪了,还得能快进、快退、回到开头。C 语言给了我们三个函数干这事:ftell、fseek、rewind。今天我就把这三个家伙掰开揉碎讲清楚。
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:比如
stdin、stdout、管道、socket。这些流不支持定位,调用会失败。 - fseek 后记得检查返回值:我曾经在一个 SD 卡读写程序里,没检查
fseek返回值,结果卡没插好,定位失败,后续读写全乱套了。 - ftell 返回 long,大文件可能溢出:如果文件超过 2GB,
ftell会返回负数。嵌入式场景一般遇不到,但心里要有数。
9.7 知识体系图
下面这张图把文件定位的核心逻辑串起来了,你一看就明白:
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 常量,再加上我提到的那些坑,你基本就能驾驭文件随机读写了。