提问人:William Pursell 提问时间:3/25/2011 最后编辑:William Pursell 更新时间:10/22/2023 访问量:289704
为什么“while( !feof(file) )”总是错的?
Why is “while( !feof(file) )” always wrong?
问:
使用来控制读取循环有什么问题?例如:feof()
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char **argv)
{
char *path = "stdin";
FILE *fp = argc > 1 ? fopen(path=argv[1], "r") : stdin;
if( fp == NULL ){
perror(path);
return EXIT_FAILURE;
}
while( !feof(fp) ){ /* THIS IS WRONG */
/* Read and process data from file… */
}
if( fclose(fp) != 0 ){
perror(path);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
这个循环有什么问题?
答:
不,这并不总是错的。如果您的循环条件是“当我们没有尝试读取文件末尾之后”时,则使用 .然而,这不是一个常见的循环条件 - 通常你想测试其他东西(例如“我可以阅读更多内容”)。 没有错,只是用错了。while (!feof(f))
while (!feof(f))
评论
f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }
f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
while(!eof(f))
feof
这是错误的,因为(在没有读取错误的情况下)它进入循环的次数比作者预期的要多。如果出现读取错误,循环永远不会终止。
请考虑以下代码:
/* WARNING: demonstration of bad coding technique!! */
#include <stdio.h>
#include <stdlib.h>
FILE *Fopen(const char *path, const char *mode);
int
main(int argc, char **argv)
{
FILE *in = argc > 1 ? Fopen(argv[1], "r") : stdin;
unsigned count = 0;
/* WARNING: this is a bug */
while( !feof(in) ) { /* This is WRONG! */
fgetc(in);
count++;
}
printf("Number of characters read: %u\n", count);
return EXIT_SUCCESS;
}
FILE *
Fopen(const char *path, const char *mode)
{
FILE *f = fopen(path, mode);
if( f == NULL ) {
perror(path);
exit(EXIT_FAILURE);
}
return f;
}
此程序将始终打印一个大于输入流中字符数的字符数(假设没有读取错误)。考虑输入流为空的情况:
$ ./a.out < /dev/null
Number of characters read: 1
在本例中,在读取任何数据之前调用,因此返回 false。输入循环,调用(并返回),并递增计数。然后被调用并返回 true,导致循环中止。feof()
fgetc()
EOF
feof()
这在所有此类情况下都会发生。 直到对流的读取遇到文件末尾后,才返回 true。的目的不是检查下一次读取是否会到达文件末尾。目的是确定上一个读取函数的状态
并区分错误条件和数据流的结束。如果返回 0,则必须使用 / 来确定是否发生了错误或是否使用了所有数据。同样,如果返回 . 仅在 fread 返回零或返回 后才有用。在此之前,将始终返回 0。feof()
feof()
feof()
fread()
feof
ferror
fgetc
EOF
feof()
fgetc
EOF
feof()
在调用 之前,始终需要检查读取的返回值(an 、 或 或 )。fread()
fscanf()
fgetc()
feof()
更糟糕的是,考虑发生读取错误的情况。在这种情况下,返回 ,返回 false,并且循环永远不会终止。在所有使用的情况下,循环内必须至少有一个检查,或者至少应该将 while 条件替换为,或者存在无限循环的非常真实的可能性,可能会在处理无效数据时喷出各种垃圾。fgetc()
EOF
feof()
while(!feof(p))
ferror()
while(!feof(p) && !ferror(p))
总而言之,尽管我不能肯定地说,在语义上写“”可能是正确的情况(尽管在循环内部必须有另一个带有中断的检查以避免读取错误时出现无限循环),但几乎可以肯定的是,它总是错误的。即使出现一个案例,它是正确的,但它在惯用语上是错误的,以至于它不是编写代码的正确方法。任何看到该代码的人都应该立即犹豫并说,“这是一个错误”。并可能打作者一巴掌(除非作者是你的老板,在这种情况下,建议酌情决定。while(!feof(f))
编辑:一种正确编写代码的方法,演示了 和 的正确用法:feof
ferror
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
int
main(int argc, char **argv)
{
FILE *in = stdin;
unsigned count = 0;
while( getc(in) != EOF ){
count++;
}
if( feof(in) ){
printf("Number of characters read: %u\n", count);
} else if( ferror(in) ){
perror("stdin");
} else {
assert(0);
}
return EXIT_SUCCESS;
}
评论
file.eof()
feof(file) || ferror(file)
feof()
指示是否尝试读取文件末尾。这意味着它几乎没有预测效果:如果它为 true,则您确定下一个输入操作将失败(顺便说一句,您不确定前一个输入操作是否失败),但如果它是 false,则您不确定下一个输入操作是否会成功。此外,输入操作可能由于文件末尾以外的其他原因而失败(格式化输入的格式错误、纯 IO 故障 - 磁盘故障、网络超时 - 对于所有输入类型),因此即使您可以预测文件末尾(以及任何尝试实现 Ada one 的人,这是预测性的, 会告诉你,如果你需要跳过空格,它可能会很复杂,并且它对交互式设备有不良影响 - 有时在开始处理前一行之前强制输入下一行),你必须能够处理失败。
所以 C 语言中正确的习惯用语是以 IO 操作成功为循环条件进行循环,然后测试失败的原因。例如:
while (fgets(line, sizeof(line), file)) {
/* note that fgets don't strip the terminating \n, checking its
presence allow to handle lines longer that sizeof(line), not showed here */
...
}
if (ferror(file)) {
/* IO failure */
} else if (feof(file)) {
/* format error (not possible with fgets, but would be with fscanf) or end of file */
} else {
/* format error (not possible with fgets, but would be with fscanf) */
}
评论
else
sizeof(line) >= 2
fgets(line, sizeof(line), file)
size <= 0
fgets(line, size, file)
sizeof(line) == 1
feof(f)
feof(f)
TL;博士
while(!feof(file))
是错误的,因为它测试了不相关的东西,而没有测试你需要知道的东西。结果是,您错误地执行了假定它正在访问已成功读取的数据的代码,而实际上这从未发生过。
我想提供一个抽象的、高层次的观点。因此,如果您对实际作用感兴趣,请继续阅读。while(!feof(file))
并发性和同时性
I/O 操作与环境交互。环境不是程序的一部分,也不在您的控制之下。环境确实与程序“同时”存在。与所有并发事件一样,关于“当前状态”的问题没有意义:在并发事件中没有“同时性”的概念。状态的许多属性根本不同时存在。
让我更准确地说:假设你想问,“你有更多的数据吗”。您可以向并发容器或 I/O 系统提出此问题。但答案通常是不可操作的,因此毫无意义。因此,如果容器说“是”——当你尝试读取时,它可能不再有数据。同样,如果答案是“否”,那么当您尝试阅读时,数据可能已经到达。结论是,根本没有像“我有数据”这样的属性,因为你无法对任何可能的答案做出有意义的回应。(缓冲输入的情况稍微好一些,可以想象,你可能会得到一个“是的,我有数据”,构成某种保证,但你仍然必须能够处理相反的情况。对于输出,情况肯定和我描述的一样糟糕:你永远不知道那个磁盘或那个网络缓冲区是否已满。
因此,我们得出结论,询问 I/O 系统是否能够执行 I/O 操作是不可能的,事实上也是不合理的。我们可以与之交互的唯一可能方法(就像与并发容器一样)是尝试操作并检查它是成功还是失败。在你与环境交互的那一刻,只有这样,你才能知道交互是否真的可行,在这一点上,你必须承诺执行交互。(如果你愿意的话,这是一个“同步点”。
EOF
现在我们进入 EOF。EOF 是从尝试的 I/O 操作中获得的响应。这意味着您正在尝试读取或写入某些内容,但是在这样做时,您无法读取或写入任何数据,而是遇到了输入或输出的末尾。基本上所有 I/O API 都是如此,无论是 C 标准库、C++ iostream 还是其他库。只要 I/O 操作成功,您就无法知道未来的操作是否会成功。您必须始终首先尝试该操作,然后对成功或失败做出响应。
例子
在每个示例中,请仔细注意,我们首先尝试 I/O 操作,然后使用结果(如果有效)。进一步注意,我们始终必须使用 I/O 操作的结果,尽管结果在每个示例中采用不同的形状和形式。
C stdio,从文件中读取:
for (;;) { size_t n = fread(buf, 1, bufsize, infile); consume(buf, n); if (n == 0) { break; } }
我们必须使用的结果是,读取的元素数(可能少至零)。
n
C stdio, :
scanf
for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) { consume(a, b, c); }
我们必须使用的结果是 的返回值,即转换的元素数。
scanf
C++,iostreams格式提取:
for (int n; std::cin >> n; ) { consume(n); }
我们必须使用的结果就是它本身,它可以在布尔上下文中进行评估,并告诉我们流是否仍处于状态。
std::cin
good()
C++,iostreams getline:
for (std::string line; std::getline(std::cin, line); ) { consume(line); }
我们必须使用的结果是 再次 ,就像以前一样。
std::cin
POSIX,用于刷新缓冲区:
write(2)
char const * p = buf; ssize_t n = bufsize; for (ssize_t k = bufsize; (k = write(fd, p, n)) > 0; p += k, n -= k) {} if (n != 0) { /* error, failed to write complete buffer */ }
我们在这里使用的结果是 ,写入的字节数。这里的重点是,我们只能知道在写入操作之后写入了多少字节。
k
POSIX
getline()
char *buffer = NULL; size_t bufsiz = 0; ssize_t nbytes; while ((nbytes = getline(&buffer, &bufsiz, fp)) != -1) { /* Use nbytes of data in buffer */ } free(buffer);
我们必须使用的结果是,最多(包括换行符)的字节数(如果文件不以换行符结尾,则为 EOF)。
nbytes
请注意,当发生错误或到达 EOF 时,该函数显式返回 (而不是 EOF!)。
-1
您可能会注意到,我们很少拼写出实际的单词“EOF”。我们通常以其他方式检测错误情况,这些方式对我们来说更直接(例如,未能执行我们期望的尽可能多的 I/O)。在每个示例中,都有一些 API 功能可以明确地告诉我们遇到了 EOF 状态,但实际上这并不是非常有用的信息。这比我们通常关心的细节要多得多。重要的是 I/O 是否成功,而不是它如何失败。
最后一个实际查询 EOF 状态的示例:假设您有一个字符串,并且想要测试它是否完整地表示一个整数,除了空格之外,末尾没有多余的位。使用 C++ iostreams,它是这样的:
std::string input = " 123 "; // example std::istringstream iss(input); int value; if (iss >> value >> std::ws && iss.get() == EOF) { consume(value); } else { // error, "input" is not parsable as an integer }
我们在这里使用两个结果。第一种是 流对象本身,用于检查格式化提取是否成功。但是,在也使用空格之后,我们执行另一个 I/O/ 操作,并期望它作为 EOF 失败,如果格式化提取已经消耗了整个字符串,则会出现这种情况。iss
value
iss.get()
在 C 标准库中,您可以通过检查结束指针是否已到达输入字符串的末尾来实现与函数类似的功能。strto*l
评论
feof()
feof()
clearerr()
feof()
不是很直观。以我非常拙见,如果任何读取操作导致到达文件末尾,则应将 的文件结束状态设置为。相反,您必须在每次读取操作后手动检查是否已到达文件末尾。例如,如果使用以下方法从文本文件中读取,则类似的东西将起作用:FILE
true
fgetc()
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *in = fopen("testfile.txt", "r");
while(1) {
char c = fgetc(in);
if (feof(in)) break;
printf("%c", c);
}
fclose(in);
return 0;
}
如果这样的东西可以工作,那就太好了:
#include <stdio.h>
int main(int argc, char *argv[])
{
FILE *in = fopen("testfile.txt", "r");
while(!feof(in)) {
printf("%c", fgetc(in));
}
fclose(in);
return 0;
}
评论
printf("%c", fgetc(in));
?这是未定义的行为。 返回 ,而不是 。fgetc()
int
char
char c
int c
"%c"
int
char
这个问题的其他答案非常好,但相当长。如果你只想要 TL;DR,是这样的:
feof(F)
名字不好。这并不意味着“现在检查是否在文件末尾”;相反,它会告诉您为什么之前的尝试无法从 .F
F
文件结束状态可以很容易地更改,因为文件可以增长或缩小,并且每次按下时终端都会报告一次(在“熟”模式下,在其他空行上)。EOF
^D
除非你真的关心为什么之前的读取没有返回任何数据,否则你最好忘记这个函数的存在。feof
上一个:C--程序未正确读取文件末尾 [duplicate]
下一个:FEOF过早成为现实
评论
feof()
来控制循环很糟糕