【问题标题】:Why is “while ( !feof (file) )” always wrong?为什么“while (!feof (file))”总是错的?
【发布时间】:2011-07-22 20:41:47
【问题描述】:

使用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 file while-loop eof feof


【解决方案1】:

TL;DR

while(!feof) 是错误的,因为它会测试一些不相关的东西,而无法测试你需要知道的东西。结果是您错误地执行了假定它正在访问已成功读取的数据的代码,而实际上这从未发生过。

我想提供一个抽象的高级观点。如果您对 while(!feof) 的实际作用感兴趣,请继续阅读。

并发性和同时性

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 标准输出,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 获取线:

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

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

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

您可能会注意到,我们很少拼出实际的单词“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
      }
    

我们在这里使用两个结果。第一个是iss,即流对象本身,用于检查格式化提取到value 是否成功。但是,在消耗完空格之后,我们执行另一个 I/O/ 操作,iss.get(),并期望它作为 EOF 失败,如果整个字符串已被格式化提取消耗,就会出现这种情况。

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

【讨论】:

  • @CiaPan:我认为这不是真的。 C99 和 C11 都允许这样做。
  • @JonathanMee:由于我提到的所有原因,这很糟糕:你无法展望未来。你无法预测未来会发生什么。
  • @JonathanMee:是的,这是合适的,但通常您可以将此检查结合到操作中(因为大多数 iostreams 操作返回流对象,该对象本身具有布尔转换),这样您清楚表明您没有忽略返回值。
  • 第三段对于一个被接受和高度赞成的答案来说是非常误导/不准确的。 feof() 不会“询问 I/O 系统是否有更多数据”。 feof(),根据 (Linux) manpage:“测试 stream 指向的流的文件结束指示符,如果设置则返回非零值。” (另外,对clearerr() 的显式调用是重置此指标的唯一方法);在这方面,William Pursell 的回答要好得多。
  • @MinhNghĩa:这是一种阻塞方法,对吧?这基本上只是围绕“尝试读取(必要时阻塞),然后报告成功状态,如果成功将读取结果存储在特殊缓冲区中”的便利包装。如果愿意,您可以在 C 和 C++ 中实现相同的功能。
【解决方案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;
    unsigned count;

    in = argc > 1 ? Fopen(argv[1], "r") : stdin;
    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) &amp;&amp; !ferror(p)),否则很有可能无限循环,可能会在处理无效数据时喷出各种垃圾。

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

【讨论】:

  • 您应该添加一个正确代码的示例,因为我想很多人会来这里寻求快速修复。
  • 这和file.eof()有区别吗?
  • @Thomas:我不是 C++ 专家,但我相信 file.eof() 返回的结果实际上与 feof(file) || ferror(file) 相同,因此非常不同。但这个问题不适用于 C++。
  • @m-ric 这也不对,因为您仍然会尝试处理失败的读取。
  • 这是实际的正确答案。 feof() 用于了解先前读取尝试的结果。因此,您可能不想将其用作循环中断条件。 +1
【解决方案3】:

不,这并不总是错误的。如果您的循环条件是“而我们还没有尝试读取文件末尾”,那么您使用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 */ }
  • @pmg:正如所说,“不是常见的循环条件”呵呵。我真的想不出我需要它的任何情况,通常我对“我能读到我想要的东西”感兴趣,这意味着错误处理
  • @pmg:如前所述,你很少需要while(!eof(f))
  • 更准确地说,条件是“当我们没有尝试读取文件末尾并且没有读取错误时”feof 不是关于检测文件结尾;它是关于确定读取是否由于错误或输入耗尽而变短。
【解决方案4】:

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
  • @AndrewHenle 你是对的!将char c 更改为int c 有效!谢谢!!
  • 第一个示例not 在从文本文件中读取时可靠地工作。如果遇到读取错误,进程将陷入无限循环,c 不断设置为 EOF,feof 不断返回 false。
  • @AndrewHenle "%c" 的哪一部分需要int,而不是char,很难理解?阅读手册页或 C 标准,其中任何一个。
  • @AndrewHenle:甚至不可能将char 参数传递给printf,因为char 类型的参数无论如何都会将get promoted 传递给int
【解决方案5】:

feof() 表示是否有人试图读取文件末尾。这意味着它几乎没有预测效果:如果它是真的,你确定下一个输入操作会失败(你不确定上一个失败 BTW),但如果它是假的,你不确定下一个输入操作操作会成功。此外,输入操作可能由于文件结尾以外的其他原因而失败(格式化输入的格式错误、纯 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) */
}

【讨论】:

  • 到达文件末尾并不是错误,因此我质疑“输入操作可能因文件末尾以外的其他原因而失败”的措辞。
  • @WilliamPursell,到达 eof 不一定是错误,但由于 eof 而无法进行输入操作就是其中之一。并且在 C 中不可能在没有输入操作失败的情况下可靠地检测 eof。
  • 最后同意else 不可能与sizeof(line) &gt;= 2fgets(line, sizeof(line), file) 但可能与病态size &lt;= 0fgets(line, size, file)。甚至可以使用sizeof(line) == 1
  • 所有那些“预测价值”的说法......我从来没有这样想过。在我的世界里,feof(f) 不会预测任何事情。它指出先前的操作已到达文件末尾。不多也不少。如果没有之前的操作(只是打开它),即使文件开始是空的,它也不会报告文件结束。因此,除了上面另一个答案中的并发解释之外,我认为没有任何理由不循环 feof(f)
  • @AProgrammer:无论是因为“永久”EOF 还是因为没有更多可用数据尚未,产生零的“最多读取 N 个字节”请求都不是一个错误。虽然 feof() 可能无法可靠地预测未来的请求会产生数据,但它可能会可靠地表明未来的请求不会。也许应该有一个状态函数来指示“未来的读取请求将成功是合理的”,其语义是在读取到普通文件的末尾之后,高质量的实现应该说未来的读取不太可能成功 absent有理由相信他们可能会
猜你喜欢
相关资源
最近更新 更多