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

使用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;
}

这个循环出了什么问题?

276548 次浏览

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

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

考虑下面的代码:

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

此程序将始终打印比输入流中的字符数大1的字符(假设没有读取错误)。考虑输入流为空的情况:

$ ./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()之前,总是有必要检查read的返回值(fread()fscanf()fgetc())。

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

因此,总而言之,尽管我不能肯定地说,从来没有一种情况下,在语义上写“while(!feof(f))"(尽管在循环中有必须的另一个检查,以避免在读取错误时出现无限循环),但它几乎肯定总是错误的。即使在某些情况下,它是正确的,但它是如此习惯性地错误,以至于它不是编写代码的正确方法。任何看到这段代码的人都应该立即犹豫并说:“这是一个bug”。可能还会扇作者一巴掌(除非作者是你的老板,在这种情况下,建议谨慎行事)。

feof()表示是否试图读取超过文件末尾的内容。这意味着它几乎没有预测效果:如果它为真,您确定下一个输入操作将失败(您不确定前一个输入操作失败BTW),但如果它为假,您不确定下一个输入操作将成功。此外,输入操作可能会因为其他原因而失败,而不是文件的结束(格式化输入的格式错误,纯IO失败-磁盘故障,网络超时-所有输入类型),所以即使你可以预测文件的结束(任何尝试实现Ada 1的人都会告诉你,如果你需要跳过空格,它会很复杂,而且它对交互设备有不良影响——有时在开始处理前一行之前强制输入下一行),您必须能够处理故障。

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

博士TL;

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

我想提供一个抽象的、高层次的视角。因此,如果您对while(!feof)的实际功能感兴趣,请继续阅读。

并发性和同时性

I/O操作与环境交互。环境不是程序的一部分,也不在您的控制之下。环境是真正同时存在的。用你的程序。与所有同时发生的事情一样,关于“当前状态”的问题;没有意义:没有“同时性”的概念。跨并发事件。状态的许多属性不能并发地使用存在

让我更精确地说:假设你想问,“你有更多的数据吗?”您可以向并发容器或I/O系统询问这个问题。但答案通常是不可行的,因此毫无意义。那么,如果容器说“yes”;-当你尝试读取时,它可能已经没有数据了。同样,如果答案是“否”,那么当你尝试阅读时,数据可能已经到达。结论是,根本不存在像“我有数据”这样的属性,因为你不能对任何可能的答案做出有意义的反应。(使用缓冲输入的情况稍微好一些,在那里您可能会得到一个“是的,我有数据”;这构成了某种保证,但你仍然要能够处理相反的情况。对于输出,情况肯定和我描述的一样糟糕:您永远不知道磁盘或网络缓冲区是否已满。)

因此我们得出结论,问I/O系统是否能够执行I/O操作是不可能的,实际上是不可能的。我们与它交互的唯一可能方式(就像与并发容器交互一样)是尝试操作并检查它是否成功或失败。在您与环境交互的那一刻,那时,也只有那时,您才能知道交互是否实际可行,在那一刻,您必须承诺执行交互。(如果你愿意,这是一个“同步点”。)

EOF

现在是EOF。EOF是从尝试 I/O操作中获得的响应。这意味着您试图读或写一些东西,但在这样做时,您无法读或写任何数据,而是遇到了输入或输出的结束。对于基本上所有的I/O api都是如此,无论是C标准库、c++ iostreams还是其他库。只要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的返回值,即转换的元素数量。

  • iostreams格式的提取:

      for (int n; std::cin >> n; ) {
    consume(n);
    }
    

    我们必须使用的结果是std::cin本身,它可以在布尔上下文中计算,并告诉我们流是否仍然处于good()状态。

  • 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,写入的字节数。这里的重点是,我们只能知道写操作写了多少字节。

  • < p > POSIX # EYZ1

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

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