问题:为什么" while(!feof(file))"总是错误的?

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

#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,feof

回答1:

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

并发性和同时性

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

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

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

EOF

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

示例

在每个示例中,请注意,我们 first 首先尝试I / O操作,然后 then 使用有效的结果。还要注意,尽管每个示例中的结果采用不同的形状和形式,但我们总是必须使用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

    请注意,当发生错误或到达EOF时,该函数显式返回-1(而不是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
    }
    

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

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

答案

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

回答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。

总是有必要检查读取的返回值(fread()fscanf()fgetc()),然后调用feof()

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

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

回答3:

不,并非总是错误。如果您的循环条件是"虽然我们还没有尝试读取文件末尾",那么您可以使用while(!feof(f))。但是,这不是常见的循环条件-通常,您要测试其他内容(例如"我可以阅读更多内容")。 while(!feof(f))没错,只是使用错误。

回答4:

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) */
}
回到顶部