TL;DR
while(!feof) 是错误的,因为它会测试一些不相关的东西,而无法测试你需要知道的东西。结果是您错误地执行了假定它正在访问已成功读取的数据的代码,而实际上这从未发生过。
我想提供一个抽象的高级观点。如果您对 while(!feof) 的实际作用感兴趣,请继续阅读。
并发性和同时性
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 == 0) { break; }
}
我们必须使用的结果是n,即已读取的元素数(可能少至零)。
-
C 标准输出,scanf:
for (int a, b, c; scanf("%d %d %d", &a, &b, &c) == 3; ) {
consume(a, b, c);
}
我们必须使用的结果是scanf的返回值,转换后的元素个数。
我们必须使用的结果是std::cin 本身,它可以在布尔上下文中求值并告诉我们流是否仍处于good() 状态。
我们必须使用的结果还是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++ 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 函数类似的功能。