为什么“while(!feof(file))”总是错的?

我见过很多人在最近很多文章中试图阅读这样的文件。

#include <stdio.h> #include <stdlib.h> int main( int argc, char **argv ) { char * path = argc > 1 ? argv[1] : "input.txt"; FILE * fp = fopen( path, "r" ); if( fp == NULL ) { perror( path ); return EXIT_FAILURE; } while( !feof( fp )) { /* THIS IS WRONG */ /* Read and process data from file… */ } fclose( fp ); return EXIT_SUCCESS; } 

这个while( !feof( fp ))循环有什么问题?

我想提供一个抽象的,高层次的视angular。

并发性和同时性

I / O操作与环境交互。 环境不是你的程序的一部分,也不在你的控制之下。 环境真正与您的程序“同时”存在。 就像所有的事情一样,关于“现状”的问题没有意义:在同时事件中没有“同时性”的概念。 许多国家的财产根本不存在

让我更精确地说:假设你想问,“你有更多的数据”。 你可以问这个并发容器,或者你的I / O系统。 但答案一般是不可行的,因此毫无意义。 那么如果容器说“是”,那么当你尝试阅读时,它可能不再有数据。 同样,如果答案是“否”,那么当您尝试阅读时,数据可能已经到达。 得出的结论是,没有像“我有数据”这样的属性,因为对于任何可能的答案你都不能采取有意义的行动。 (缓冲input的情况会稍微好一点,你可能会想到一个“是的,我有数据”,这是一种保证,但是你仍然必须能够处理相反的情况。肯定和我描述的一样糟糕:你永远不知道该磁盘或networking缓冲区是否已满。)

所以我们得出这样的结论:询问I / O系统是否能够执行I / O操作是不可能的,而且事实上是不合理的 。 我们可以与之交互的唯一可能的方式(就像并发容器一样)是尝试操作并检查它是成功还是失败。 在那个时候你和环境进行交互,那么只有这样你才能知道交互是否真的有可能,并且在那一刻你必须承诺执行交互。 (如果你愿意,这是一个“同步点”。)

EOF

现在我们到了EOF。 EOF是您从尝试的 I / O操作获得的响应 。 这意味着您正在尝试读取或写入某些内容,但这样做时,您无法读取或写入任何数据,而是遇到input或输出的结尾。 基本上所有的I / O API都是如此,无论是C标准库,C ++ iostream还是其他库。 只要I / O操作成功,你根本无法知道未来的进一步操作是否会成功。 您必须先尝试操作,然后回应成功或失败。

例子

在每个例子中,请注意,我们首先尝试I / O操作, 然后在结果有效的情况下使用结果。 进一步注意的是,我们总是必须使用I / O操作的结果,尽pipe结果在每个例子中采用了不同的形状和forms。

  • C stdio,从文件中读取:

     for (;;) { size_t n = fread(buf, 1, bufsize, infile); consume(buf, n); if (n < bufsize) { 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本身,它可以在布尔上下文中求值,并告诉我们stream是否仍然处于good()状态。

  • C ++,iostreams getline:

     for (std::string line; std::getline(std::cin, line); ) { consume(line); } 

    我们必须使用的结果又是std::cin ,就像以前一样。

  • 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 ,写入的字节数。 这里的要点是,我们只能知道在写操作之后写了多less个字节。

  • 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); 

    我们必须使用的结果是nbytes ,直到并包括换行符的字节数(如果文件没有以换行符结尾,则为EOF)。

    请注意,当发生错误或EOF时,函数显式返回-1 (而不是EOF!)。

您可能会注意到我们很less拼出实际的单词“EOF”。 我们通常以一些其他方式来检测错误情况,这些方式对我们来说更为直接有趣(例如,没有按照我们的期望执行尽可能多的I / O操作)。 在每个例子中都有一些API函数可以明确地告诉我们EOF状态已经遇到,但事实上这并不是一个非常有用的信息。 这比我们经常关心的要多得多。 重要的是I / O是否成功,更重要的是如何失败。

  • 实际查询EOF状态的最后一个示例:假设您有一个string,并且想要testing它是否完整表示一个整数,除了空格外,末尾没有额外的位。 使用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 } 

    我们在这里使用两个结果。 首先是iss对象本身,检查格式化的提取value成功。 但是,在消耗空白之后,我们执行另一个I / O操作iss.get() ,并期望它失败,因为EOF,如果整个string已经被格式化的提取消耗掉了。

    在C标准库中,通过检查结束指针是否已经到达inputstring的末尾,可以实现与strto*l函数类似的function。

答案

while(!eof)是错误的,因为它testing的东西是无关紧要的,无法testing你需要知道的东西。 其结果是,您错误地执行代码,假定它正在访问成功读取的数据,实际上这从来没有发生过。

这是错误的,因为(在没有读取错误的情况下)它比作者期望的多一次进入循环。 如果有读取错误,循环不会终止。

考虑下面的代码:

 /* 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; unsigned count; in = argc > 1 ? Fopen( argv[ 1 ], "r" ) : stdin; count = 0; /* WARNING: this is a bug */ while( !feof( in )) { /* This is WRONG! */ (void) 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; } 

这个程序会一直打印比inputstream中的字符数更大的字符(假设没有读取错误)。 考虑inputstream为空的情况:

 $ ./a.out < /dev/null Number of characters read: 1 

在这种情况下, feof()在任何数据读取之前被调用,所以它返回false。 循环被input, fgetc()被调用(并返回EOF ),count递增。 然后调用feof()并返回true,导致循环中止。

这在所有这些情况下都会发生。 feof()不会返回true,直到stream的读取遇到文件的结尾。 feof()的目的不是检查下一次读取是否会到达文件末尾。 feof()的目的是区分读取错误和到达文件结尾。 如果fread()返回0,则必须使用feof / ferror来决定。 同样如果fgetc返回EOFfeof() fread已经返回0或fgetc已经返回EOF 之后才有用。 在这之前, feof()将始终返回0。

在调用feof()之前,总是需要检查read( fread() ,或者fscanf()或者fgetc() )的返回值。

更糟的是,考虑发生读取错误的情况。 在这种情况下, fgetc()返回EOFfeof()返回false,循环不会终止。 在所有使用while(!feof(p))情况下, ferror()至less应该在循环内部进行ferror() ,或者至lesswhile条件应该用while(!feof(p) && !ferror(p))或者存在一个无限循环的非常现实的可能性,当处理无效的数据时可能会浪费各种垃圾。

所以,总之,虽然我不能肯定地说,在写“ while(!feof(f)) ”的情况下,从来没有一种情况可能在语义上是正确的(尽pipe在循环中必须有一个中断避免读取错误造成无限循环),这种情况几乎肯定是错误的。 即使有一个案例出现的地方是正确的,但是这是不正确的方式来写代码。 任何人看到这些代码应该立即犹豫,并说,“这是一个错误”。 并可能对作者进行打击(除非作者是你的老板,在这种情况下,build议酌情处理)

不,这并不总是错的。 如果你的循环条件是“而我们还没有尝试读取文件的结尾”,那么你使用while (!feof(f)) 。 然而,这不是一个常见的循环条件 – 通常你想testing其他的东西(比如“我能读更多”)。 while (!feof(f))没有错,只是使用错了。

feof()表示是否尝试读取文件的结尾。 这意味着它没什么预测作用:如果它是真的,你确定下一个input操作会失败(你不确定前一个input是否失败),但是如果它是假的,你不确定下一个input操作会成功。 此外,input操作可能由于文件结尾之外的其他原因而失败(格式化input的格式错误,纯IO错误 – 磁盘故障,networking超时 – 所有inputtypes),所以即使您可以预测文件的结尾(以及任何试图实现Ada的预测的人都会告诉你,如果你需要跳过空格,它会对交互设备产生不良影响 – 有时会强迫下一个input在开始处理前一行之前),你必须能够处理失败。

所以C语言中的正确习惯是把IO操作成功作为循环条件循环,然后testing失败的原因。 例如:

 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) */ } 

很好的答案,我只是注意到同样的事情,因为我正在试图做一个这样的循环。 所以,在这种情况下是错误的,但是如果你想在EOF处有一个优雅的循环,这是一个很好的方法:

 #include <stdio.h> #include <sys/stat.h> int main(int argc, char *argv[]) { struct stat buf; FILE *fp = fopen(argv[0], "r"); stat(filename, &buf); while (ftello(fp) != buf.st_size) { (void)fgetc(fp); } // all done, read all the bytes }