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

我见过很多人在最近很多帖子中试图阅读这样的文件。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
    char * path = argc > 1 ? argv[1] : "input.txt";

    FILE * fp = fopen(path, "r");
    if( fp == NULL ) {
        perror(path);
        return EXIT_FAILURE;
    }

    while( !feof(fp) ) {  /* THIS IS WRONG */
        /* Read and process data from file… */
    }
    if( fclose(fp) == 0 ) {
        return EXIT_SUCCESS;
    } else {
        perror(path);
        return EXIT_FAILURE;
    }
}

while( !feof(fp))循环有什么问题?


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

并发性和同时性

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 < 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 ,就像以前一样。

  • 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函数类似的功能。

  • 答案

    while(!eof)是错误的,因为它测试的是无关紧要的东西,并且无法测试您需要知道的某些内容。 其结果是,您错误地执行假定它正在访问已成功读取数据的代码,而实际上这种情况从未发生过。


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

    考虑下面的代码:

    /* 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! */
            (void) fgetc( in );
            count++;
        }
        printf( "Number of characters read: %un", 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 ),count递增。 然后调用feof()并返回true,导致循环中止。

    这在所有这些情况下都会发生。 feof()不直到的数据流的读取遇到文件末尾返回true。 feof()的目的不是检查下一次读取是否会到达文件末尾。 feof()的目的是区分读取错误和到达文件结尾。 如果fread()返回0,则必须使用feof / ferror来决定。 同样如果fgetc返回EOFfeof() fread已返回零或fgetc已返回EOF后才有用。 在此之前, feof()将始终返回0。

    在调用feof()之前,总是需要检查读取的返回值( fread() ,或fscanf()fgetc() feof()

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

    因此,总之,虽然我不能肯定地说,从来没有一种情况可能在语法上写“ while(!feof(f)) ”(尽管在循环中必须有一个中断避免读取错误造成无限循环),这种情况几乎肯定总是错误的。 即使有一个案例出现的地方是正确的,但它是如此地道的错误,它不会是编写代码的正确方法。 任何人看到该代码应立即犹豫并说,“这是一个错误”。 并且可能会打击作者(除非作者是你的老板,在这种情况下,建议酌情决定)。


    不,它并不总是错的。 如果你的循环条件是“虽然我们还没有尝试读取文件的结尾”,那么你使用while (!feof(f)) 。 然而这不是一个常见的循环条件 - 通常你想测试其他的东西(比如“我能读更多”)。 while (!feof(f))没有错,它只是用错了。

    链接地址: http://www.djcxy.com/p/19783.html

    上一篇: Why is “while ( !feof (file) )” always wrong?

    下一篇: What is the most efficient way to RESET an SQLite Table