【C语言加油站】C语言文件操作完全指南:feof、ferror与缓冲区机制详解
结束判定
- 导读
- 一、`feof`
- 1.1 函数介绍
- 1.2 函数使用
- 二、`ferror`
- 2.1 函数介绍
- 2.2 函数使用
- 三、`clearerr`
- 3.1 函数介绍
- 3.2 函数的使用
- 四、文件缓冲区
- 4.1 缓冲区介绍
- 4.2 `fflush`
- 4.3 函数使用
- 结语
导读
大家好,很高兴又和大家见面啦!!!
在上一篇博客中,我们深入探讨了C语言文件操作中的定位函数,包括
- 如何使用
fseek
进行精确定位 - 通过
ftell
获取当前文件位置 - 利用
rewind
快速返回到文件开头
这些函数共同构成了文件随机读写的基石,让我们能够在文件中自由导航。
然而,仅仅掌握文件定位是不够的——在实际文件操作过程中,我们经常会遇到一些关键问题:
- 如何准确判断何时到达文件末尾?
- 如何识别和处理可能发生的错误?
- 为什么有时文件操作的结果与预期不符?
这些问题的答案都离不开对文件结束判定和缓冲区机制的深入理解。
今天,我们就将围绕这些实际问题展开探讨,帮助大家掌握文件操作中的状态判定技巧。
一、feof
1.1 函数介绍
该函数的用法如下:
- 向函数中传入一个参数:
stream
:指向识别流的FILE
对象指针
- 函数会在该流中检测,是否设置了文件末尾指示器:
- 已设置,返回一个非零值
- 未设置,返回
0
1.2 函数使用
该函数的用法简单的理解就是检查当前流中是否设置了文件末尾指示器。
这里我们需要注意的是文件末尾指示器表示的并不是光标处于文件的末尾
怎么来理解文件末尾指示器与光标处于文件末尾这二者的区别呢?
下面我们看图:
上图中展示的是光标处于文件末尾,但是此时并没有设置 eof
,因此我们通过 feof
进行检测时的结果应该是 0
;
当我们在该位置进行了一次读取操作后:
可以看到,该位置上多了一个 eof
标志,这就表示此时设置了一个 eof
指示器,我们可以通过 feof()
检测出来;
下面我们就来通过代码来对其进行测试,进一步验证我们的说法:
void test1() {FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "r");if (pf == NULL) {perror("fopen");return;}// 通过 fgetc 移动int ch = fgetc(pf);// 文件中的最后一个字符是 'a',因此我们将字符a作为循环结束标志while (ch != 'a') {printf("ch = %c->%3d\n", ch, ch);ch = fgetc(pf);}// 通过 feof 检测是否设置了 eofint set_eof = feof(pf);if (set_eof == 0) {printf("\n当前光标处于文件末尾,即字符a的后面\n");printf("\nch = %c->%3d\n", ch, ch);// 再一次在该位置进行读取操作ch = fgetc(pf);// 再一次检测 eofset_eof = feof(pf);}// eof 的整型值为 -1,我们通过 ch 的值来观察if (set_eof) {printf("ch = %3d\n", ch);}fclose(pf);
}
这里的测试逻辑比较简单:
- 先通过
fgetc
函数的读取操作,将光标移动至文件的末尾 - 再通过
feof
检测一下是否设置了文件末尾指示器- 没有设置,则再一次在该位置进行读取操作
- 已设置,则观察此时
ch
的值是否为 -1
下面我们就来看一下测试结果:
可以看到,当 fgetc
函数读取完 a
后,并不会直接设置 eof
,但是此时的光标确实位于 a
的右侧,也就是文件末尾;
当我们再一次对该位置进行读取操作后,此时光标仍处于该位置,但不同的是,这时流中设置了 eof
,因此我们通过 feof
可以检测出来;
这里需要注意:
光标所在的位置一定是在数据的左侧,我们在进行读取/写入数据时,光标会在其右侧进行数据的读取/写入操作
当完成对应操作后,光标会往右侧进行移动
这里我们通过图示来说明光标的位置:
再直观一点,大家可以打开一个文本编辑器,然后你可以看一下,每完成一次输入,光标是否位于输入的内容右侧!!!这里我就不给大家演示了,大家可以自己动手操作一下。
回到正题,从上面的测试中我们可以得到两个结论:
- 当光标位于文件末尾时,流中不一定设置了
eof
- 当此时我们对该位置进行一次读取操作时,流中会设置
eof
,并且我们可以通过feof
检测出来
这个函数的使用比较简单,下面我们再来看一下另一个指示器函数 ferror
;
二、ferror
2.1 函数介绍
该函数的用法如下:
- 向函数中传入一个参数:
stream
:指向识别流的FILE
对象指针
- 函数会在该流中检测,是否设置了错误指示器:
- 已设置,返回一个非零值
- 未设置,返回
0
2.2 函数使用
从函数的用法介绍中,我们不难看到,ferror
与 feof
的使用方法是一致的,他们之间的区别就是检测的对象不同:
feof
检测的是文件末尾指示器eof
ferror
检测的是错误指示器error
在前面的介绍中我们有介绍过,对于 fgetc
,不管是发生了读取错误,还是读取到了文件末位,其返回值都是 EOF
因此我们使用该函数时,一定要对 EOF
的类型进行判定:
void test2() {FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data_.txt", "w");if (pf == NULL) {perror("fopen");return;}// 通过 fgetc 读取文件中的内容int ch = fgetc(pf);if (ch == -1) {int set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);int set_error = ferror(pf);printf("\nset_error = %d\n", set_error);}else {printf("ch = %c -> %d\n", ch, ch);}fclose(pf);
}
这次的测试逻辑也很简单:
- 我们通过进写入模式
w
打开文件data_.txt
- 此时文件中的原内容会被清除,并等待写入,这时的光标位于文件开头,也是文件的末尾
- 之后我们通过
fgetc
进行一次读取操作- 若读取成功,则函数返回的
EOF
可以被feof
检测 - 若读取失败,则函数返回的
EOF
可以被ferror
检测
- 若读取成功,则函数返回的
下面我们就来看一下测试结果:
可以看到,对于仅写入模式打开的文件,我们是无法进行读取操作的,因此 fgetc
在进行读取操作时会发生错误,并设置 error
,我们可以通过 ferror
检测出来;
三、clearerr
不管是 eof
还是 error
,当文件中存在这两个指示器时,我们都无法对文件进行下一步的操作,那如果我想继续对文件进行写入,应该如何处理呢?
这里就涉及到了一个内容—— 指示器清除操作;
在上一篇的内容中我们介绍了3个能够清除指示器的函数:
fseek
能够清除eof
fsetpos
能够清除eof
rewind
能够清除eof
和error
这三者的清除原理都相同——通过改变位置指示器(光标)的位置来清除流中设置的eof
和 error
那如果我们不想移动光标,直接将指示器清除,可以做到吗?
答案是当然可以,这里就可以调用 clearerr
来清除这些指示器;
3.1 函数介绍
该函数的用法如下:
- 向函数中传入一个参数:
stream
:指向识别流的FILE
对象指针
- 函数会清除流中已经设置的错误指示器和文件末尾指示器
3.2 函数的使用
这里我们通过直接在前面的测试代码中加入 clearerr
,之后再进行指示器检测:
void test3() {FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data_.txt", "w");if (pf == NULL) {perror("fopen");return;}// 通过 fgetc 读取文件中的内容int ch = fgetc(pf);if (ch == -1) {int set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);int set_error = ferror(pf);printf("\nset_error = %d\n", set_error);// 通过 clearerr 清除指示器clearerr(pf);set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);set_error = ferror(pf);printf("\nset_error = %d\n", set_error);}else {printf("ch = %c -> %d\n", ch, ch);}fclose(pf);
}
下面我们直接来看测试结果:
可以看到,clearerr
完成了错误指示器的清除。
四、文件缓冲区
在介绍这个内容前,我们先来看一段代码:
void test4() {FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data.txt", "r");if (pf == NULL) {perror("fopen");return;}// 通过 fgetc 读取文件中的内容int ch = fgetc(pf);for (int i = 0; i < 3; i++) {printf("ch = %c -> %3d\n", ch, ch);ch = fgetc(pf);}// 检测此时流中是否存在指示器printf("\n检测流中是否存在指示器:\n");int set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);int set_error = ferror(pf);printf("\nset_error = %d\n", set_error);// 若不存在指示器if (set_eof == 0 && set_error == 0) {printf("\nch = %c -> %3d\n", ch, ch);// 向文件中写入字符 ! ch = fputc('!', pf);}// 若返回值为 -1 ,检测是哪种类型的指示器if (ch == -1) {printf("\n检测指示器类型:\n");// 检测指示器的类型set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);set_error = ferror(pf);printf("\nset_error = %d\n", set_error);// 清除指示器clearerr(pf);printf("\n清除指示器后,再一次检测是否存在指示器:\n");set_eof = feof(pf);printf("\nset_eof = %d\n", set_eof);set_error = ferror(pf);printf("\nset_error = %d\n", set_error);}else {printf("\nch = %c -> %d\n", ch, ch);}fclose(pf);
}
这时有朋友会好奇,这段代码不就是测试 clearerr
函数的吗?有啥好看的?
别着急,下面我们来看一下运行结果:
可以看到,正常情况下,由于文件是 r
模式打开,因此我们是无法写入成功的,所以 fputc
的返回值应该是 EOF
并且流中还会设置一个 error
指示器。
但是在本次的测试中,fputc
的返回值表示写入成功,而我们打开文件后发现,文件中并没有被写入 !
这是为什么呢?
这个就是我们现在要介绍的——文件缓冲区.
4.1 缓冲区介绍
ANSIC 标准采用 缓冲文件系统 处理的数据文件,所谓缓冲文件系统是指系统自动地在程序中为每一个正在使用的文件开辟一块 文件缓冲区 。
- 从内存向磁盘中输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
- 从磁盘向计算机读取数据,则从磁盘文件中将读取的数据输入到内存缓冲区,然后再从缓冲区逐个地将数据送到程序数据区
简单的理解,文件缓冲区 就是文件数据的一个 中转站:
上图中的 输出缓冲区 指的是我们通过函数向文件写入数据时系统开辟的一块 文件缓冲区;
输入缓冲区 指的是我们通过函数从文件中读取数据时系统开辟的一块 文件缓冲区;
也就是说,我们不管是向文件写入数据,还是从文件中读取数据,系统并不会直接实现这一操作,而是通过缓冲区这一中转站,临时存放写入或读取的数据,之后再一起将所有的数据转移到其对应的位置;
这就能解释,为什么前面的测试中,fputc
函数的返回值不是 EOF
而是我们写入的 !
对应的 ASCII
码值—— 33
。
这是因为函数在执行写入操作时,实际上是先将数据写入到 缓冲区 中,此时的行为肯定是写入成功的,因此 fputc
函数返回了写入成功时的值;
而当我们结束程序运行后,通过 fclose
关闭了文件,此时系统会将缓冲区的数据写入到文件中,但是由于之前我们是以 r
模式(只读)打开的文件,也就是说,此时的文件是不接收写入操作,因此数据才没有写入成功;
4.2 fflush
该函数的用法如下:
- 向函数中传入一个参数:
stream
:指向特定缓冲区的流的FILE
对象指针
- 函数会刷新与其关联的缓冲区:
- 刷新成功,返回
0
- 刷新失败,返回
EOF
并设置error
指示器
- 刷新成功,返回
4.3 函数使用
从函数的介绍中我们可以知道,该函数主要是作用于与流关联的缓冲区:
- 以写入方式或读写方式打开的文件,在使用该函数后,输出缓冲区中的数据会全部写入文件中;
- 以读取方式打开的文件中,使用该函数的取决于具体的实现:
- 在一些库的实现中是将输入缓冲区的内容全部清除;
- 而在一些不支持该操作的库中,通过
fflush
来清除输入缓冲区的行为是无效的;
下面我们就来简单测试一下该函数:
void test5() {FILE* pf = fopen("C:\\Users\\LIQIAN\\Desktop\\data_.txt", "w");if (pf == NULL) {perror("fopen");return;}// 写入数据printf("写入数据:\n");for (int i = 0; i < 3; i++) {int ch = 'a' + i;// 通过 fputc 写入数据后,光标会向后移动ch = fputc(ch, pf);printf("ch = %c -> %3d\n", ch, ch);}// 更新缓冲区,将输出缓冲区中的内容写入文件中int flush = fflush(pf);printf("flush = %d\n", flush);fclose(pf);
}
为了更好的观察 fflush
的工作过程,我们这里采用调式的方式进行查看:
此时,代码执行到了 159
行,也就是还未通过 fflush
更新缓冲区,这时如果我们打开文件,我们就可以看到,此时的文件中不存在任何数据;
当我们继续往后执行时,此时执行到了 162
行,也就是我们还未执行 fclose
关闭文件的操作,这时我们再打开文件时,我们就能看到文件中已经写入了我们先前写入的数据;
从这一次的测试中我们就能清晰的感受到 fflush
的作用——将缓冲区中的数据写入文件中;
结语
今天的内容到这里就全部结束了。通过本文的学习,我们深入探讨了C语言文件操作中两个关键而常被误解的主题:文件结束判定和缓冲区机制。掌握这些知识对于编写健壮、可靠的文件操作代码至关重要。
下面让我们简要回顾一下本文的核心内容:
-
feof
函数:用于检测文件末尾指示器是否被设置。关键点在于:光标位于文件末尾并不等同于设置了eof
指示器。只有在尝试读取超出文件末尾的数据后,eof
指示器才会被设置。因此,while (!feof(fp))
是一个常见的错误用法。 -
ferror
函数:用于检测文件操作过程中是否发生了错误。当文件读写函数(如fgetc
)返回EOF
时,应使用ferror
来区分是遇到了文件末尾还是发生了错误。 -
clearerr
函数:用于清除流的错误指示器和文件末尾指示器。当需要在不移动文件指针的情况下重置流状态以便继续操作时,这个函数非常有用。 -
文件缓冲区:C语言使用缓冲文件系统,数据读写会先经过内存中的缓冲区。这解释了为何有时写入操作看似成功(数据已进入缓冲区),但文件内容并未立即更新。fflush函数可以强制将输出缓冲区的数据立即写入磁盘。
理解这些机制有助于避免常见的编程陷阱:
-
正确判断文件读取结束的条件,应首先检查读写函数(如
fgetc
)的返回值,再使用feof
或ferror
来确定结束的具体原因。 -
意识到缓冲区的存在,在需要确保数据立即写入磁盘时(如日志记录),及时使用
fflush
或妥善关闭文件。
✨ 如果觉得本文对您有帮助,请点个赞支持一下吧! 您的肯定是我持续分享的动力。
🔔 想了解更多C语言和编程技巧?欢迎关注我! 后续会带来更多深度、实用的技术文章。
📢 觉得文章有价值?不妨转发给更多需要的朋友! 知识共享,共同进步。
💾 遇到文件操作难题?赶紧收藏本文吧! 方便日后查阅,随时复习这些关键概念。
💬 有任何疑问、见解或想分享的经验吗? 欢迎在评论区留言交流,我们一起探讨,共同成长!
感谢您的阅读,期待下次再见!🚀