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 知识体系:随机读写的核心逻辑
下面这张图帮你理清这三个函数的关系:
5.7 避坑指南
最后,分享几个我踩过的坑:
- 文本模式下的偏移量:在Windows上,文本模式下换行符
\n会被转换为\r\n。这时候用fseek()计算偏移量可能会出错。我建议二进制文件用二进制模式打开,文本文件用文本模式,别混用。 - 不要对 stdout/stdin 用 fseek():标准输入输出流不一定支持随机定位。我曾经试图对
stdin用fseek(),结果程序直接崩溃。 - fseek() 的返回值一定要检查:返回0表示成功,非0表示失败。别偷懒不检查,否则定位失败了你都不知道。
- 大文件注意 long 的范围:在32位系统上,
long最大只能表示2GB。处理大文件时要用fseeko()和ftello(),它们用off_t类型,支持更大的文件。
我的习惯:每次调用 fseek() 后,我都会紧跟着调用 ftell() 确认位置是否正确。虽然多了一行代码,但能避免很多隐蔽的bug。
好了,关于随机读写就讲这么多。记住,fseek() 是定位工具,ftell() 是测量工具,rewind() 是复位工具。三者配合,你就能像操作数组一样灵活地操作文件了。
公众号:蓝海资料掘金营,微信deep3321