【C语言-文件操作】3. 文件的定位与状态检测——移动指针、判断结尾

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 5
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

前面两节课,咱们把文件的打开、关闭、读写都讲完了。但有个问题一直没解决——文件读着读着,你怎么知道读到哪儿了?

就好比你读一本书,你手里得有个书签吧?不然你读到第几页了都不知道,想翻回去重新看一段也找不着地方。文件操作也一样,你得能知道当前读到哪个位置了,能跳到指定位置去读,能从头再来一遍。

今天咱们就讲怎么在文件里“跳来跳去”,以及怎么判断文件读完了、出错了该怎么办。

文件位置指针——文件里的“隐形书签”

每个打开的文件,系统都维护了一个“位置指针”。它记录着当前读到哪儿了。你每读一个字符,它就往后挪一格。你每写一个字符,它也往后挪一格。

这个“位置指针”你看不见摸不着,但所有读写操作都跟它有关。你打开文件的时候,位置指针在开头(如果是追加模式,在末尾)。你读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回到开头读。亲手试试在文件里“跳来跳去”是什么感觉。

下节课咱们把文件操作的知识全串起来,写一个完整的持久化程序——学生信息管理系统,能存能读,关了程序再打开数据还在。那才是文件操作的真正实战。