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

674

使用 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;
}

这个循环有什么问题?


28
为什么使用 feof() 控制循环是不好的?使用 feof() 函数来控制循环可能会导致错误。feof() 函数在文件结尾返回 true,因此当文件被读取完毕时,它将返回 true。但是,在循环中使用 feof() 不能保证在读取到文件结尾之前结束循环,因为当文件结尾被读取时,下一次迭代将开始并尝试读取数据,导致读取错误和未定义行为。作为替代方案,最好使用文件 I/O 函数的返回值来控制循环。例如,在读取文件时,可以使用 fgets() 函数,如果返回 NULL,则表示已经读取到文件结尾。这种方法可以确保循环结束的正确性,并且避免了潜在的错误。 - Grijesh Chauhan
1
为什么在循环条件中使用iostream::eof被认为是错误的? - Jonathan Wakely
6个回答

546

简而言之

while(!feof(file)) 是错误的,因为它测试了一个无关紧要的条件,却没有测试你需要知道的条件。结果是你错误地执行了假设已成功读取数据的代码,而实际上从未发生过这种情况。

我想提供一个抽象的、高层次的视角。所以如果你对 while(!feof(file)) 到底做了什么感兴趣,就继续阅读。

并发和同时性

I/O 操作与环境进行交互。环境不是你的程序的一部分,也不在你的控制之下。环境与你的程序真正地"并发"存在。与所有并发事件一样,关于"当前状态"的问题是没有意义的:在并发事件之间不存在"同时性"的概念。许多状态的属性在并发中根本不存在。

让我更明确一下:假设你想问,“你有更多的数据吗?”你可以向并发容器或者你的I/O系统提出这个问题。但是答案通常是无法采取行动的,因此毫无意义。所以,如果容器回答“是”——当你尝试读取时,它可能已经没有数据了。同样地,如果答案是“否”,当你尝试读取时,数据可能已经到达。结论是,根本就没有像“我有数据”这样的属性,因为你无法对任何可能的答案做出有意义的反应。(在缓冲输入方面情况稍微好一些,你可能会得到一个“是的,我有数据”的回答,这构成了某种保证,但你仍然必须能够处理相反的情况。而且在输出方面,情况肯定和我描述的一样糟糕:你永远不知道那个磁盘或者网络缓冲区是否已满。)
所以我们得出结论,询问一个I/O系统是否能够执行I/O操作是不可能的,事实上也是不合理的。我们唯一能够与它进行交互的方式(就像与并发容器一样)是尝试进行操作并检查是否成功或失败。只有在与环境进行交互的那一刻,你才能知道交互是否真正可能,并且在那时你必须决定执行交互。这是一个“同步点”,如果你愿意这样称呼的话。
现在我们来谈谈EOF。EOF是你从尝试的I/O操作中得到的响应。它意味着你试图读取或写入某些数据,但在这样做时,你未能读取或写入任何数据,而是遇到了输入或输出的结束。这对于几乎所有的I/O API都是适用的,无论是C标准库、C++ iostreams还是其他库。只要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)
      {
          /* 使用缓冲区中的nbytes数据 */
      }
      free(buffer);
    

    我们必须使用的结果是nbytes,即包括换行符(或文件以EOF结尾时的EOF)在内的字节数。

    请注意,当发生错误或达到EOF时,该函数明确返回-1(而不是EOF)。

你可能会注意到,我们很少拼写出实际的单词"EOF"。我们通常以其他更有趣的方式检测错误条件(例如,无法执行我们所期望的输入/输出操作)。在每个示例中,都有一些API功能可以明确告诉我们已经遇到了EOF状态,但事实上,这并不是一个非常有用的信息。这更多是一个细节,而我们通常并不太关心。重要的是I/O是否成功,而不是它是如何失败的。
一个最后的例子实际上查询了EOF状态:假设你有一个字符串,并且想要测试它是否完全表示一个整数,除了空白字符以外没有额外的位。使用C++的iostreams,代码如下:
std::string input = " 123 "; // 例子
std::istringstream iss(input); int value; if (iss >> value >> std::ws && iss.get() == EOF) { consume(value); } else { // 错误,"input"无法解析为整数 }
我们在这里使用了两个结果。第一个是iss,即流对象本身,用于检查对value的格式化提取是否成功。但是,在消耗空格之后,我们执行另一个I/O操作iss.get(),并期望它失败并返回EOF,如果整个字符串已经被格式化提取消耗完毕的话,这就是情况。
在C标准库中,您可以通过检查结束指针是否达到输入字符串的末尾来实现类似的功能,使用strto*l函数。

38
@CiaPan:我不认为那是真的。C99和C11都允许这样做。 - Kerrek SB
4
由于我提到的原因,这是不好的:你无法预知未来,也无法预测未来会发生什么。 - Kerrek SB
4
是的,那样做是合适的,不过通常你可以将这个检查与操作结合起来(因为大多数iostreams操作会返回流对象本身,它本身具有布尔转换功能),这样你就可以明确地表明你没有忽略返回值。 - Kerrek SB
14
对于一个被接受且高度点赞的回答来说,第三段非常误导/不准确。feof()并不是“询问I/O系统是否有更多数据”。根据(Linux)manpage,“测试由指向流的stream指针所指示的流的文件结束标志,如果已设置,则返回非零值。”(此外,显式调用clearerr()是重置该结束标志的唯一方法)。在这方面,William Pursell的回答要好得多。 - Arne Vogel
4
是的,那是一种阻塞方法。基本上它只是一个方便的封装,其实现方式为“尝试读取(必要时阻塞),然后报告成功状态,如果成功则将读取结果存储在特殊缓冲区中”。如果您愿意,您可以在C和C++中实现相同的功能。 - Kerrek SB
显示剩余16条评论

279
这是错误的,因为(在没有读取错误的情况下),它比作者预期的循环多执行一次。如果存在读取错误,循环将永远不会终止。
考虑以下代码:
/* 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

在这种情况下,在读取任何数据之前调用了feof(),所以它返回false。进入循环,调用fgetc()(并返回EOF),并增加计数。然后调用feof()并返回true,导致循环中止。
这种情况在所有类似的情况下都会发生。只有在流上的读取遇到文件结束时,feof()才会返回true。 feof()的目的不是检查下一次读取是否会达到文件末尾。 feof()的目的是确定先前读取函数的状态,并区分错误条件和数据流的结束。如果fread()返回0,则必须使用feof / ferror来决定是否发生错误或是否消耗了所有数据。同样,如果fgetc返回EOFfeof()只有在fread返回零或fgetc返回EOF之后才有用。在此之前,feof()始终返回0。

在调用feof()之前,始终需要检查读取的返回值(无论是fread()fscanf()还是fgetc())。

更糟糕的是,考虑一种情况,即发生读取错误。在这种情况下,fgetc()返回EOFfeof()返回false,并且循环永远不会终止。在使用while(!feof(p))的所有情况下,必须在循环内部至少进行一次ferror()检查,或者至少将while条件替换为while(!feof(p) && !ferror(p)),否则可能会出现无限循环的情况,可能会产生各种垃圾数据作为无效数据进行处理。
总之,虽然我不能确定永远不会出现语义上正确编写"while(!feof(f))"的情况(尽管在发生读取错误时必须在循环内部进行另一个检查并使用break来避免无限循环),但几乎可以肯定它总是错误的。即使有一种情况下它是正确的,它也是如此习惯上错误,以至于不是编写代码的正确方式。任何看到那段代码的人都应该立即犹豫并说:“那是个错误”。并可能批评作者(除非作者是你的上司,这时请谨慎行事)。
编辑:正确编写代码的一种方法,演示了feofferror的正确用法:
#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;
}

107
请添加一份正确代码的示例,因为我想很多人会来这里寻找快速解决方案。 - jleahy
1
这与 file.eof() 不同吗? - Thomas
6
@Thomas:我不是 C++ 专家,但我认为 file.eof() 返回的实际上与 feof(file) || ferror(file) 返回的结果相同,所以两者非常不同。但这个问题并不适用于 C++。 - William Pursell
6
@ m-ric 这也不对,因为你仍然会尝试处理一个失败的读取。 - Mark Ransom
5
这是实际正确的答案。feof()用于了解上一个读取尝试的结果。因此,您可能不希望将其用作循环终止条件。+1 - Jack
显示剩余9条评论

78

不,它并不总是错误的。如果你的循环条件是“尚未尝试读取文件末尾”,那么你可以使用while (!feof(f))。然而,这不是一个常见的循环条件-通常你想测试其他东西(例如“我能否继续读取”)。while (!feof(f))并不是错误的,它只是被使用不当


1
我想知道... f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ } 或者(将要测试)f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ } - pmg
1
@pmg:就像你所说的,“不是常见的循环条件”哈哈。我真的想不出我需要它的任何情况,通常我只关心“我能读到我想要的内容吗”,这意味着错误处理。 - Erik
如@pmg所说,你很少想要使用while(!eof(f)) - Erik
11
更准确地说,此条件是“在我们尝试读取文件末尾之前且没有读取错误的情况下”。feof并不是用来检测文件结束的,它是用来确定读取是否因为错误或者输入已经耗尽而变短了。 - William Pursell

47

feof()函数用于判断是否已经尝试读取超出文件末尾的内容。这意味着它的预测能力有限:如果它返回true,你可以确定下一次输入操作将失败(顺便说一下,你不确定前一次操作是否失败);但如果它返回false,则不能确定下一次输入操作是否成功。此外,输入操作可能因为其他原因而失败(例如格式化输入的格式错误,所有输入类型的纯IO故障——磁盘故障、网络超时等),因此即使你可以预测文件末尾的位置(并且任何尝试实现具有预测功能的Ada的人都会告诉你,如果需要跳过空格,那么它可能会变得很复杂,并且对交互设备有不良影响——有时会强制在处理上一个输入行之前输入下一行),你仍然必须能够处理失败。

因此,在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) */
}

2
到达文件结尾并不是一个错误,因此我对“输入操作可能因文件结束以外的其他原因而失败”的措辞表示质疑。 - William Pursell
@WilliamPursell,到达eof并不一定是错误,但由于eof而无法进行输入操作是错误的。在C中,如果没有进行输入操作失败,则无法可靠地检测到eof。 - AProgrammer
同意最后一个 elsesizeof(line) >= 2fgets(line, sizeof(line), file) 不可能,但在病态的 size <= 0fgets(line, size, file) 中是可能的。甚至可能使用 sizeof(line) == 1 - chux - Reinstate Monica
3
“预测价值”的谈话……我从没这样想过。在我的世界里,“feof(f)”并没有预测任何事情。它只是说明先前的操作已到达文件结尾,仅此而已。如果没有先前的操作(只是打开了文件),即使文件一开始就是空的,它也不会报告文件已结束。因此,除了上面另一个答案中提到的并发解释外,我认为没有理由不在“feof(f)”上循环。 - BitTickler
1
@AProgrammer:一个“读取N个字节”的请求,如果由于“永久”EOF或者没有更多的数据可用而产生零,则不是错误。虽然feof()可能无法可靠地预测未来的请求是否会产生数据,但它可以可靠地表明未来的请求不会产生数据。也许应该有一个状态函数,指示“未来的读取请求可能成功”,其语义是在读取普通文件的末尾后,优质实现应该说未来的读取不太可能成功,除非有理由相信它们可能成功。 - supercat
@AProgrammer:在实现中将文件结束视为短暂条件的情况因实现而异,但典型原因可能是文件以“r”或“rb”模式打开,同时也以“w”或“wb”模式打开。 - supercat

2
其他回答对这个问题都很好,但是有点长。如果你只想要简短的回答,就是这样:
"feof(F)" 这个函数的命名不太好。它并不意味着“检查文件 F 是否已经到达文件末尾”,而是告诉你为什么之前的尝试未能从文件 F 中获取任何数据。
文件的结束状态很容易改变,因为文件可以增长或缩小,而终端在每次按下“^D”(在“cooked”模式下,在一个空行上)时报告 EOF。
除非你真的关心之前的读取为什么未能返回任何数据,否则最好忘记 feof 函数的存在。

-2

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;
}

4
这段代码printf("%c", fgetc(in));是未定义行为。fgetc()返回的类型是int而不是char。请注意修改。 - Andrew Henle
1
@AndrewHenle 你说得对!将 char c 改为 int c 就可以了!谢谢!! - Scott Deagan
1
第一个示例在从文本文件读取时不可靠。如果您遇到读取错误,该进程将陷入无限循环中,c 常被设置为 EOF,feof 不断返回 false。 - William Pursell
2
@AndrewHenle 难以理解 "%c" 的哪一部分期望 int 而不是 char?请阅读 manpage 或 C 标准中任何一个。 - 12431234123412341234123
2
@AndrewHenle:甚至不可能将char参数传递给printf,因为char类型的参数将会被提升int - Andreas Wenzel
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接