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


573

我最近在很多帖子中都看到有人试图读取这样的文件:

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

这个循环有什么问题?



Answers:


453

我想提供一个抽象的高层视角。

并发性和同时性

I / O操作与环境交互。环境不是您的程序的一部分,也不在您的控制之下。该环境真正与您的程序“同时存在”。与所有并发事件一样,关于“当前状态”的问题也没有道理:在并发事件中没有“同时性”的概念。状态的许多属性根本不会同时存在

让我更精确地说:假设您想问“您是否有更多数据”。您可以询问并发容器或I / O系统。但是答案通常是不可行的,因此毫无意义。因此,如果容器说“是”,该怎么办?到您尝试读取时,它可能不再有数据。同样,如果答案为“否”,那么在您尝试阅读时,数据可能已经到达。结论是,仅存在没有“我有数据”之类的属性,因为您无法对任何可能的答案做出有意义的举动。(使用缓冲输入的情况要好一些,可以想象得到“是的,我有数据”可以构成某种保证,但是您仍然必须能够处理相反的情况。通过输出该情况肯定和我描述的一样糟糕:您永远不知道该磁盘或网络缓冲区是否已满。)

因此,我们得出结论,这是不可能的,而事实上未合理的,要问的I / O系统是否能够执行I / O操作。我们与之交互的唯一可能方法(就像与并发容器一样)是尝试操作并检查其成功还是失败。在与环境进行交互的那一刻,只有那时,您才能知道该交互是否确实可行,并且在这一点上,您必须致力于执行交互。(如果需要,这是一个“同步点”。)

紧急行动

现在我们到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 < 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本身,可以在布尔上下文中对其求值,并告诉我们流是否仍处于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);

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

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

您可能会注意到,我们很少拼出实际的单词“ EOF”。我们通常以其他一些我们更感兴趣的方式检测错误情况(例如,无法执行所需的I / O)。在每个示例中,都有一些API功能可以明确告诉我们已经遇到了EOF状态,但实际上这并不是一条非常有用的信息。它比我们经常关心的细节更多。重要的是I / O是否成功,而不是失败如何。

  • 最后一个实际查询EOF状态的示例:假设您有一个字符串,并且想要测试它是否完整地表示一个整数,除了空格,末尾没有多余的位。使用C ++ iostream,它是这样的:

    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
    }

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

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

答案

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


34
@CiaPan:我认为那不是真的。C99和C11都允许这样做。
Kerrek SB 2015年

11
但是ANSI C没有。
CiaPan 2015年

3
@JonathanMee:出于我提到的所有原因,这很糟糕:您无法展望未来。您无法判断将来会发生什么。
Kerrek SB 2015年

3
@JonathanMee:是的,这是适当的,尽管通常您可以将此检查合并到操作中(因为大多数iostreams操作都返回流对象,该对象本身具有布尔转换),因此您可以很明显地看出来忽略返回值。
Kerrek SB 2015年

4
第三段对于接受并高度评价的答案非常误导/不准确。feof()不会“询问I / O系统是否有更多数据”。feof(),根据(Linux)联机帮助页:“测试流指向的流的文件结束指示符,如果设置了,则返回非零值。” (同样,显式调用clearerr()是重置此指示器的唯一方法);在这方面,威廉·珀塞尔的答案要好得多。
Arne Vogel

234

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

考虑以下代码:

/* 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 。的目的不是检查下一次读取是否到达文件末尾。的目的是区分读取错误和到达文件末尾。如果返回0,则必须使用/ 来确定是否遇到错误或是否消耗了所有数据。如果返回则类似。 仅 fread返回零或返回后才有用。在此之前,将始终返回0。feof()feof()fread()feofferrorfgetcEOFfeof()fgetcEOFfeof()

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

更糟糕的是,考虑发生读取错误的情况。在这种情况下,fgetc()return EOFfeof()return false和循环永远不会终止。在所有使用情况下,while(!feof(p))都必须在循环内至少检查ferror(),或者至少应将while条件替换为,while(!feof(p) && !ferror(p))否则很可能会发生无限循环,可能会产生各种垃圾,例如无效数据正在处理中。

因此,总而言之,尽管我不能确定地说永远不会出现写“ while(!feof(f))” 在语义上正确的情况(尽管循环内必须进行另一次检查,但要有一个中断以避免在读取错误时发生无限循环) ),这种情况几乎肯定总是错误的。即使出现了正确的案例,这也是非常错误的习惯,以至于它不是编写代码的正确方法。任何看到该代码的人都应立即犹豫并说:“那是一个错误”。并可能对作者打耳光(除非作者是您的老板,在这种情况下,建议您谨慎行事。)


7
当然,这是错误的-但除此之外,它也不是“非常丑陋”。
nobar

89
您应该添加一个正确代码的示例,因为我想很多人会来这里寻求快速修复。
jleahy13年

6
@Thomas:我不是C ++专家,但是我相信file.eof()返回的结果实际上与相同feof(file) || ferror(file),因此非常不同。但是,这个问题并不适用于C ++。
威廉·珀塞尔

6
@ m-ric也不正确,因为您仍将尝试处理失败的读取。
Mark Ransom

4
这是实际的正确答案。feof()用于了解先前读取尝试的结果。因此,可能您不想将其用作循环中断条件。+1
杰克

63

不,并非总是错误的。如果您的循环条件是“虽然我们还没有尝试读取文件末尾”,那么您可以使用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 2011年

1
@pmg:正如所说,“不是常见的循环条件”呵呵。我真的想不到我需要的任何情况,通常我对“我能阅读我想要的内容”感兴趣,并带有所有错误处理的含义
Erik

@pmg:正如您所说,您很少想要while(!eof(f))
Erik

9
更准确地说,条件是“虽然我们没有尝试读取文件末尾并且没有读取错误”, feof但这并不是要检测文件末尾。它是关于确定读取是由于错误而短还是由于输入已用尽。
威廉·珀塞尔

35

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
到达文件末尾不是错误,因此我质疑措辞“输入操作可能由于文件末尾以外的其他原因而失败”。
威廉·珀塞尔

@WilliamPursell,到达eof不一定是错误,但是由于eof而无法执行输入操作是一个。而且在C中,如果不使输入操作失败,就不可能可靠地检测eof。
AProgrammer 2012年

最后else不可能同意sizeof(line) >= 2fgets(line, sizeof(line), file)但可能同意病理size <= 0和疾病fgets(line, size, file)。甚至有可能sizeof(line) == 1
chux-恢复莫妮卡2015年

1
所有这些“预测性价值”都在谈论……我从来没有那样想过。在我的世界中,feof(f)不会预测任何东西。它指出PREVIOUS操作已到达文件末尾。仅此而已。并且如果没有先前的操作(只是打开了它),即使文件是空的,它也不会报告文件结束。因此,除了上面另一个答案中的并发解释之外,我认为没有任何理由不继续讨论feof(f)
BitTickler

@AProgrammer:A“读取多达N个字节”请求产率零,无论是因“永久” EOF或因为没有更多的数据是可用的,不是错误。尽管feof()可能无法可靠地预测将来的请求将产生数据,但它可以可靠地表明将来的请求将不会产生数据。也许应该有一个状态函数来指示“将来的读取请求可能会成功”,其语义是在读取到普通文件的末尾之后,一个高质量的实现应该说,如果没有某些原因,将来的读取就不太可能成功。相信他们会的
超级猫

0

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
安德鲁·亨利

在我看来,标准习语while( (c = getchar()) != EOF)非常“像这样”。
威廉·珀塞尔

while( (c = getchar()) != EOF)可以在运行GNU C 10.1.0的桌面之一上运行,但不能在运行GNU C 9.3.0的Raspberry Pi 4上运行。在我的RPi4上,它不会检测到文件结尾,并且会继续运行。
Scott Deagan
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.