前面两节课,咱们把文件的打开、关闭、读写都讲完了。但有个问题一直没解决——文件读着读着,你怎么知道读到哪儿了?
就好比你读一本书,你手里得有个书签吧?不然你读到第几页了都不知道,想翻回去重新看一段也找不着地方。文件操作也一样,你得能知道当前读到哪个位置了,能跳到指定位置去读,能从头再来一遍。
今天咱们就讲怎么在文件里“跳来跳去”,以及怎么判断文件读完了、出错了该怎么办。
文件位置指针——文件里的“隐形书签”
每个打开的文件,系统都维护了一个“位置指针”。它记录着当前读到哪儿了。你每读一个字符,它就往后挪一格。你每写一个字符,它也往后挪一格。
这个“位置指针”你看不见摸不着,但所有读写操作都跟它有关。你打开文件的时候,位置指针在开头(如果是追加模式,在末尾)。你读N个字符,位置指针就往后走N个位置。下次读的时候从新位置开始读。
你想控制这个位置指针,就得用今天要讲的三个函数:fseek、ftell、rewind。
ftell——告诉我现在读到哪儿了
ftell是最简单的一个,它的作用是返回当前文件位置指针相对于文件开头的偏移量(字节数)。
long ftell(FILE *fp);
成功返回当前偏移量,失败返回-1L。
FILE *fp = fopen("test.txt", "r"); if (fp == NULL) { perror("打开失败"); return 1; } // 打开文件的时候,位置指针在开头,偏移量是0 printf("当前位置:%ld\n", ftell(fp)); // 输出0 // 读一个字符,位置指针往后走一格 fgetc(fp); printf("读了一个字符后:%ld\n", ftell(fp)); // 输出1 // 再读三个字符 fgetc(fp); fgetc(fp); fgetc(fp); printf("又读了三个字符后:%ld\n", ftell(fp)); // 输出4 fclose(fp);
你可以用ftell来算文件大小——把位置指针移到文件末尾,然后读ftell的值:
FILE *fp = fopen("test.txt", "rb"); fseek(fp, 0, SEEK_END); // 移到文件末尾 long size = ftell(fp); // 当前位置就是文件大小 printf("文件大小:%ld字节\n", size); fclose(fp);
这个技巧很常用,记一下。
fseek——跳到指定位置
fseek是文件定位里最重要的函数。它让你把位置指针移到任何你想去的地方。
int fseek(FILE *fp, long offset, int origin);
三个参数:
-
fp:文件指针 -
offset:偏移量,正数往后移,负数往前移 -
origin:从哪里开始算,有三个可选值:-
SEEK_SET:从文件开头算(值为0) -
SEEK_CUR:从当前位置算(值为1) -
SEEK_END:从文件末尾算(值为2)
-
成功返回0,失败返回非0。
几个典型的用法:
移到文件开头:
fseek(fp, 0, SEEK_SET); // 从开头偏移0,就是文件开头
移到文件末尾:
fseek(fp, 0, SEEK_END); // 从末尾偏移0,就是文件末尾
往后跳10个字节:
fseek(fp, 10, SEEK_SET); // 从开头往后10个字节
往前退5个字节:
fseek(fp, -5, SEEK_CUR); // 从当前位置往前5个字节
完整例子:跳着读文件
假设文件里存的是ABCDEFGHIJ十个字符,你想先读第3个字符,再读第7个字符:
#include <stdio.h> int main() { FILE *fp = fopen("test.txt", "r"); if (fp == NULL) { perror("打开失败"); return 1; } // 跳到第3个位置(下标从0开始,第3个就是'C') fseek(fp, 2, SEEK_SET); printf("第3个字符:%c\n", fgetc(fp)); // 输出C // 跳到第7个位置 fseek(fp, 6, SEEK_SET); printf("第7个字符:%c\n", fgetc(fp)); // 输出G fclose(fp); return 0; }
注意:fseek只适用于二进制文件或者文本文件的按字节定位。 文本文件因为可能有换行符转换(Windows下\n会变成\r\n),fseek的偏移量可能不准。所以如果你要精确按位置跳,用二进制模式("rb")打开文件。
rewind——一键回到开头
rewind就是fseek(fp, 0, SEEK_SET)的简化版,把位置指针直接拉回文件开头。
void rewind(FILE *fp);
FILE *fp = fopen("test.txt", "r"); char ch; // 读一遍文件 while ((ch = fgetc(fp)) != EOF) { putchar(ch); } // 回到开头,再读一遍 rewind(fp); printf("\n--- 再读一遍 ---\n"); while ((ch = fgetc(fp)) != EOF) { putchar(ch); } fclose(fp);
rewind不会返回任何值,也不检查错误,就是简单粗暴地回到开头。适合“读完了想从头再读一遍”的场景。
feof——文件读完了吗?
feof用来判断文件是否已经读到了末尾。
int feof(FILE *fp);
如果文件已经到达末尾,返回非0(真);否则返回0(假)。
但是这里有个大坑,无数人踩过:
FILE *fp = fopen("test.txt", "r"); char ch; while (!feof(fp)) { // 错误用法! ch = fgetc(fp); printf("%c", ch); } fclose(fp);
这个循环会多打印一个字符。为啥?
因为feof判断的是“上一次读操作是否读到了末尾”。它不是“预测下一次读会不会到末尾”。当你读完最后一个字符的时候,feof还是返回假——因为你刚刚成功读到了最后一个字符,还没尝试读下一个呢。然后循环再来一次,fgetc尝试读下一个,返回EOF,printf把这个EOF打印出来(乱码),然后feof才返回真,循环退出。
正确写法:
FILE *fp = fopen("test.txt", "r"); int ch; while ((ch = fgetc(fp)) != EOF) { // 直接判断读到的值 printf("%c", ch); } fclose(fp);
不要用feof来控制循环,而是用读函数的返回值来判断。 fgetc返回EOF、fgets返回NULL、fread返回小于请求的数量——这些都是判断结尾的正确方式。
feof的正确用法是在循环结束之后,判断“这次结束是因为读到了末尾,还是因为出错了”:
while ((ch = fgetc(fp)) != EOF) { // 处理字符 } if (feof(fp)) { printf("正常读到文件末尾\n"); } else if (ferror(fp)) { printf("读取出错\n"); }
ferror——出错了没有?
ferror用来判断文件操作是否发生了错误。
int ferror(FILE *fp);
如果发生了错误,返回非0;否则返回0。
FILE *fp = fopen("test.txt", "r"); int ch; while ((ch = fgetc(fp)) != EOF) { putchar(ch); } if (ferror(fp)) { printf("读取文件时发生错误\n"); } else { printf("读取正常结束\n"); }
feof和ferror要配合使用。 循环退出的时候,你不知道是因为读完了还是出错了。feof告诉你“读完了”,ferror告诉你“出错了”。如果两个都返回假,说明有其他问题。
perror——把错误信息打印出来
perror你之前已经见过了,它能把系统给的错误原因打印出来。
void perror(const char *s);
你传一个字符串进去,它会先打印这个字符串,然后加个冒号,再把当前系统错误信息打印出来。
FILE *fp = fopen("nonexistent.txt", "r"); if (fp == NULL) { perror("打开文件失败"); // 输出:打开文件失败: No such file or directory }
perror会自动读取全局变量errno,根据它的值找到对应的错误描述。你不需要自己判断错误类型。
你也可以在文件读写出错的时候用perror:
FILE *fp = fopen("test.txt", "w"); if (fprintf(fp, "hello") < 0) { perror("写入失败"); }
一个完整的例子:文件定位综合练习
#include <stdio.h> int main() { FILE *fp = fopen("test.txt", "w+"); // 读写模式,文件不存在就创建 if (fp == NULL) { perror("打开失败"); return 1; } // 写入一些内容 fputs("ABCDEFGHIJ", fp); printf("写入完成,当前文件大小:%ld字节\n", ftell(fp)); // 回到开头 rewind(fp); // 读前三个字符 printf("前三个字符:"); for (int i = 0; i < 3; i++) { printf("%c", fgetc(fp)); } printf("\n"); // 跳到第6个位置 fseek(fp, 5, SEEK_SET); printf("第6个字符:%c\n", fgetc(fp)); // 从末尾往前退3个字节 fseek(fp, -3, SEEK_END); printf("最后三个字符:"); while (!feof(fp)) { int ch = fgetc(fp); if (ch != EOF) putchar(ch); } printf("\n"); fclose(fp); return 0; }
这个例子里,你先写入了ABCDEFGHIJ,然后回到开头读前三个,跳到第6个位置读一个,最后从末尾往前退3个字节读到结尾。把fseek、ftell、rewind全用上了。
你跑一下这个程序,看看输出是不是你预期的。
几个常见的坑
坑1:用feof控制循环
前面说过了,这是最经典的错误。用读函数的返回值来判断,别用feof。
坑2:fseek的偏移量类型写错
fseek(fp, 10, SEEK_SET); // 正确 fseek(fp, 10L, SEEK_SET); // 也正确,加L表示long fseek(fp, 10.5, SEEK_SET); // 错误,偏移量必须是整数
坑3:对文本文件用fseek定位
Windows下文本文件的换行符是\r\n(两个字节),但C语言读的时候会转成\n(一个字节)。你fseek按字节跳,可能会跳到\r和\n中间,读出来就是乱的。要精确按位置定位,用二进制模式打开。
坑4:rewind之后忘了重置错误标志
rewind会清除文件结束标志,但不清除错误标志。如果之前出过错,ferror还会返回真。一般没什么大问题,但知道一下。
坑5:fseek之后没检查返回值
if (fseek(fp, 100, SEEK_SET) != 0) { printf("定位失败\n"); }
fseek可能失败(比如偏移量超出了文件范围),检查一下更稳妥。
总结一下
这节课的核心就三组函数:
定位三兄弟:
-
ftell:我现在在哪儿? -
fseek:我要跳到某个位置。 -
rewind:一键回到开头。
状态检测:
-
feof:读完了吗?(别用来控制循环!) -
ferror:出错了没有? -
perror:把错误信息打印出来。
记住一个原则:用读函数的返回值判断是否结束,循环退出后再用feof和ferror来判断是正常结束还是出错。
回去自己动手,把文件定位的几个例子都跑一遍。写一个程序,先写10个字符到文件,然后用fseek跳到第5个位置读,再用rewind回到开头读。亲手试试在文件里“跳来跳去”是什么感觉。
下节课咱们把文件操作的知识全串起来,写一个完整的持久化程序——学生信息管理系统,能存能读,关了程序再打开数据还在。那才是文件操作的真正实战。