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

Why is “while( !feof(file) )” always wrong?

提问人:William Pursell 提问时间:3/25/2011 最后编辑:William Pursell 更新时间:10/22/2023 访问量:289704

问:

使用来控制读取循环有什么问题?例如: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;
}

这个循环有什么问题?

C 文件 while-loop eof feof

答:

78赞 Erik 3/25/2011 #1

不,这并不总是错的。如果您的循环条件是“当我们没有尝试读取文件末尾之后”时,则使用 .然而,这不是一个常见的循环条件 - 通常你想测试其他东西(例如“我可以阅读更多内容”)。 没有错,只是错了。while (!feof(f))while (!feof(f))

评论

1赞 pmg 3/25/2011
我想知道...... 或者(要测试这个)f = fopen("A:\\bigfile"); while (!feof(f)) { /* remove diskette */ }f = fopen(NETWORK_FILE); while (!feof(f)) { /* unplug network cable */ }
1赞 Erik 3/25/2011
@pmg:如前所述,“不是常见的循环条件”呵呵。我真的想不出我需要它的任何情况,通常我对“我能读出我想要的东西吗”感兴趣,所有这些都意味着错误处理
0赞 Erik 3/25/2011
@pmg:如前所述,你很少想要while(!eof(f))
11赞 William Pursell 7/3/2013
更准确地说,条件是“虽然我们没有尝试读取文件末尾并且没有读取错误”,但与检测文件末尾无关;它是关于确定读取是否由于错误或输入耗尽而缩短。feof
279赞 William Pursell 3/25/2011 #2

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

请考虑以下代码:

/* 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()EOFfeof()

这在所有此类情况下都会发生。 直到对流的读取遇到文件末尾,才返回 true。的目的不是检查下一次读取是否会到达文件末尾。目的是确定上一个读取函数的状态 并区分错误条件和数据流的结束。如果返回 0,则必须使用 / 来确定是否发生了错误或是否使用了所有数据。同样,如果返回 . 仅在 fread 返回零或返回 后才有用。在此之前,将始终返回 0。feof()feof()feof()fread()feofferrorfgetcEOFfeof()fgetcEOFfeof()

在调用 之前,始终需要检查读取的返回值(an 、 或 或 )。fread()fscanf()fgetc()feof()

更糟糕的是,考虑发生读取错误的情况。在这种情况下,返回 ,返回 false,并且循环永远不会终止。在所有使用的情况下,循环内必须至少有一个检查,或者至少应该将 while 条件替换为,或者存在无限循环的非常真实的可能性,可能会在处理无效数据时喷出各种垃圾。fgetc()EOFfeof()while(!feof(p))ferror()while(!feof(p) && !ferror(p))

总而言之,尽管我不能肯定地说,在语义上写“”可能是正确的情况(尽管在循环内部必须有另一个带有中断的检查以避免读取错误时出现无限循环),但几乎可以肯定的是,它总是错误的。即使出现一个案例,它是正确的,但它在惯用语上是错误的,以至于它不是编写代码的正确方法。任何看到该代码的人都应该立即犹豫并说,“这是一个错误”。并可能打作者一巴掌(除非作者是你的老板,在这种情况下,建议酌情决定。while(!feof(f))

编辑:一种正确编写代码的方法,演示了 和 的正确用法: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 7/13/2013
您应该添加一个正确代码的示例,因为我想很多人会来这里寻找快速修复。
1赞 Thomas 8/27/2014
这与 ?file.eof()
6赞 William Pursell 8/27/2014
@Thomas:我不是C++专家,但我相信 file.eof() 返回的结果与 有效相同,因此非常不同。但这个问题并不适用于 C++。feof(file) || ferror(file)
6赞 Mark Ransom 4/10/2015
@m-ric 这也是不对的,因为您仍然会尝试处理失败的读取。
5赞 Jack 1/29/2017
这是实际的正确答案。feof() 用于了解上一次读取尝试的结果。因此,您可能不想将其用作循环中断条件。+1
47赞 AProgrammer 2/10/2012 #3

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

评论

2赞 William Pursell 9/29/2012
到达文件末尾不是错误,所以我质疑“输入操作可能因文件末尾以外的其他原因而失败”的措辞。
0赞 AProgrammer 9/29/2012
@WilliamPursell,到达 EOF 不一定是错误,但由于 EOF 而无法执行输入操作是其中之一。在 C 语言中,如果不进行输入操作失败,就不可能可靠地检测 eof。
0赞 chux - Reinstate Monica 3/27/2015
最后同意 not possible with and but possible with pathological and .甚至可能使用 .elsesizeof(line) >= 2fgets(line, sizeof(line), file)size <= 0fgets(line, size, file)sizeof(line) == 1
3赞 BitTickler 9/25/2017
所有“预测价值”都在谈论......我从来没有这样想过。在我的世界里,没有预测任何事情。它指出 PREVIOUS 操作已命中文件末尾。仅此而已。如果没有先前的操作(刚刚打开它),即使文件开始时为空,它也不会报告文件末尾。所以,除了上面另一个答案中的并发解释之外,我认为没有任何理由不循环。feof(f)feof(f)
1赞 supercat 2/3/2019
@AProgrammer:产生零的“读取最多 N 字节”请求,无论是因为“永久”EOF 还是因为还没有更多数据可用,都不是错误。虽然 feof() 可能无法可靠地预测未来的请求将产生数据,但它可能会可靠地指示未来的请求不会。也许应该有一个状态函数来指示“未来的读取请求可能会成功”,其语义是,在读取到普通文件的末尾后,高质量的实现应该说未来的读取不太可能成功,除非有理由相信它们可能会成功。
546赞 Kerrek SB 10/25/2014 #4

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::cingood()

  • 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 失败,如果格式化提取已经消耗了整个字符串,则会出现这种情况。issvalueiss.get()

在 C 标准库中,您可以通过检查结束指针是否已到达输入字符串的末尾来实现与函数类似的功能。strto*l

评论

38赞 Kerrek SB 1/29/2015
@CiaPan:我不认为这是真的。C99 和 C11 都允许这样做。
4赞 Kerrek SB 2/4/2015
@JonathanMee:由于我提到的所有原因,这很糟糕:你无法展望未来。你无法预知未来会发生什么。
4赞 Kerrek SB 2/4/2015
@JonathanMee:是的,这是合适的,尽管通常您可以将此检查合并到操作中(因为大多数 iostreams 操作返回流对象,该对象本身具有布尔转换),这样您就可以明显地表明您不会忽略返回值。
15赞 Arne Vogel 9/10/2018
第三段对于一个被接受和高度赞成的答案来说是非常具有误导性的/不准确的。 不会“询问 I/O 系统是否有更多数据”。,根据 (Linux) 手册页:“测试流指向的流的文件结束指示符,如果设置了,则返回非零。(此外,显式调用 to 是重置此指示器的唯一方法);在这方面,威廉·珀塞尔的答案要好得多。feof()feof()clearerr()
4赞 Kerrek SB 5/31/2019
@MinhNghĩa:这是一种阻止方法,对吧?这基本上只是一个方便的包装器,围绕着“尝试读取(必要时阻止),然后报告成功状态,如果成功,则将读取结果存储在一个特殊的缓冲区中”。如果你愿意,你可以在 C 和 C++ 中实现相同的功能。
-2赞 Scott Deagan 6/8/2020 #5

feof()不是很直观。以我非常拙见,如果任何读取操作导致到达文件末尾,则应将 的文件结束状态设置为。相反,您必须在每次读取操作后手动检查是否已到达文件末尾。例如,如果使用以下方法从文本文件中读取,则类似的东西将起作用:FILEtruefgetc()

#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赞 Andrew Henle 6/8/2020
printf("%c", fgetc(in));?这是未定义的行为。 返回 ,而不是 。fgetc()intchar
1赞 Scott Deagan 6/9/2020
@AndrewHenle 你是对的!变成作品!谢谢!!char cint c
1赞 William Pursell 7/29/2020
第一个示例在从文本文件读取时无法可靠地工作。如果遇到读取错误,进程将卡在一个无限循环中,c 不断设置为 EOF,feof 不断返回 false。
2赞 12431234123412341234123 10/3/2020
@AndrewHenle 期望 a 而不是 的哪一部分很难理解?阅读手册页或 C 标准,其中任何一个。"%c"intchar
2赞 Andreas Wenzel 11/6/2020
@AndrewHenle:甚至不可能将参数传递给 ,因为无论如何,类型的参数都会被提升为 。charprintfcharint
2赞 Martin Kealey 10/20/2023 #6

这个问题的其他答案非常好,但相当长。如果你只想要 TL;DR,是这样的:

feof(F)名字不好。这并不意味着“现在检查是否在文件末尾”;相反,它会告诉您为什么之前的尝试无法从 .FF

文件结束状态可以很容易地更改,因为文件可以增长或缩小,并且每次按下时终端都会报告一次(在“熟”模式下,在其他空行上)。EOF^D

除非你真的关心为什么之前的读取没有返回任何数据,否则你最好忘记这个函数的存在。feof