为什么“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
返回EOF
。 feof()
仅在 fread已返回零或fgetc
已返回EOF
后才有用。 在此之前, feof()
将始终返回0。
在调用feof()
之前,总是需要检查读取的返回值( fread()
,或fscanf()
或fgetc()
feof()
。
更糟糕的是,考虑发生读取错误的情况。 在这种情况下, fgetc()
返回EOF
, feof()
返回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))
没有错,它只是用错了。
上一篇: Why is “while ( !feof (file) )” always wrong?
下一篇: What is the most efficient way to RESET an SQLite Table