4. 文件读写位置:ftell与fseek、rewind、feof与ferror、文件指针的移动与随机读写
文件操作中,读写位置是个容易被忽视但极其重要的概念。说白了,文件就像一盘磁带,你读到哪里、写到哪里,全靠一个「位置指针」来标记。我刚开始学C语言时,总觉得文件读写就是从头到尾顺序来一遍,直到有一次做日志分析程序,需要在几百MB的文件里来回跳着读数据,才真正体会到位置控制的重要性。
4.1 文件位置指针的概念
每个打开的文件,系统都会维护一个内部指针,指向当前要读写的字节位置。你调用 fread 或 fwrite 时,数据从这个位置开始操作,操作完成后指针自动后移。这个机制是透明的,但你可以通过几个函数来主动控制它。
核心理解:文件位置指针不是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:检查状态
文件操作中,判断「是否读到末尾」和「是否出错」是两个常见需求。feof 和 ferror 就是干这个的。
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 在最后一次读取失败后才返回真,但循环体已经执行了一次无效的读取。你想想看,这会导致最后一批数据被重复处理,或者读到垃圾数据。
个人经验:我习惯在每次读取操作后立即检查返回值,而不是依赖 feof 或 ferror。这两个函数更多用于区分「正常结束」和「出错结束」。比如,fread 返回0,可能是文件末尾,也可能是读取错误,这时用 feof 和 ferror 就能区分。
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 计算偏移量,读取后检查 feof 和 ferror 来确认状态。
4.7 知识体系图
下面这张图总结了文件位置控制的核心函数和它们之间的关系:
4.8 避坑指南
最后,分享几个我踩过的坑:
- 不要混合使用文本模式和二进制模式:在Windows上,文本模式会做换行符转换,导致
ftell返回的位置和实际字节数不一致。处理二进制数据时,始终用"rb"或"wb"。 - fseek 到文件末尾后,不要直接写数据:如果你用
fseek(fp, 0, SEEK_END)定位到末尾,然后调用fwrite,数据会追加到文件末尾。但如果你先读后写,位置指针会移动,要小心。 - rewind 会清除错误标志:如果你在读取过程中发生了错误,调用
rewind后错误标志会被清除。这有时是好事,有时会掩盖问题。我一般先检查ferror,再决定是否rewind。 - feof 不是预测函数:它不会告诉你「下一次读取是否会到达末尾」,而是告诉你「上一次读取是否因为到达末尾而失败」。这个区别很重要。
文件位置控制是C语言文件IO的精髓之一。掌握了 ftell、fseek、rewind、feof 和 ferror,你就能像操作数组一样灵活地读写文件。说白了,就是把文件当成一个巨大的字节数组,用位置指针当下标,想读哪里读哪里。
公众号:蓝海资料掘金营,微信deep3321