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

标签 c file while-loop 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;
}


这个循环有什么问题?

最佳答案

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

并发性和同时性

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

请注意,当发生错误或到达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)是错误的,因为它测试不相关的内容并且无法测试您需要知道的内容。结果是您错误地执行了假定代码正在访问已成功读取的数据的代码,而实际上却从未发生过。

关于c - 为什么“while(!feof(file))”总是错误的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47465193/

相关文章:

c - 我的 C 中的 decToBase 方法出现错误并返回

c# - 获取除 .zip 之外的所有文件

c++ - 不确定这个 while 语句在做什么

c - 将数据动态复制到二维字符数组时发生访问冲突

c - C语言中使用switch语句fork进程的程序

c - 如何在 Ceedling 中使用 gdb?

c# - 使用 System.IO.Directory.Delete 删除目录

java - OutputStreamWriter.append 不将文本附加到文本文件 Android 编程

php - 仅发布使用 while 循环创建的表单中的选择值

javascript - Vanilla Js : Start a while loop in one event, 用第二个事件打破它? ('mouseover'开始, 'mouseout'设置循环条件为假)