为什么在循环条件中使用iostream::eof(例如`while (!stream.eof())`)被认为是错误的?

686
我刚在这个回答中发现一条评论,称在循环条件中使用iostream::eof几乎肯定是错误的。我通常使用类似于while(cin>>n)的方式来读取输入,并且这种方式隐式地检查了EOF。

为什么明确检查EOF时使用while (!cin.eof())是错误的呢?它与在C语言中使用scanf("...",...)!=EOF有何不同(我通常都这样做而没有问题)?


26
在C语言中,“scanf(...) != EOF”也无法正常工作,因为“scanf”的返回值是成功解析和赋值的字段数。正确的条件是“scanf(...) < n”,其中“n”是格式字符串中的字段数量。 - Ben Voigt
7
如果到达 EOF,它将返回一个负数(通常将EOF定义为这样)。 - Sebastian
22
实际上,如果在第一个字段转换(无论成功与否)之前遇到文件结尾,它会返回“EOF”。 如果在字段之间达到文件结尾,则会返回成功转换和存储的字段数。 这使得将其与“EOF”进行比较是错误的。 - Ben Voigt
3
是的,对于这种情况(读取一个简单整数),可以这样做。但是很容易想到一种情况,即“while(fail)”循环以实际故障和终止eof。 想象一下如果每次迭代需要3个整数(比如你要读取x-y-z点之类的东西),但流中错误地只有两个整数。 - sly
1
这个问题类似于并且有着与C语言问题相同的答案:为什么while(!feof(file))总是错误的?。因为标志位只有在到达EOF之后才会被设置。 - legends2k
显示剩余4条评论
5个回答

611

因为 iostream::eof 只有在读取到流的结尾后才会返回 true。它并不表示下一次读取就是流的结尾。

考虑以下情况(假设下一次读取将在流的结尾处):

while(!inStream.eof()){
  int data;
  // yay, not end of stream yet, now read ...
  inStream >> data;
  // oh crap, now we read the end and *only* now the eof bit will be set (as well as the fail bit)
  // do stuff with (now uninitialized) data
}

反之:
int data;
while(inStream >> data){
  // when we land here, we can be sure that the read was successful.
  // if it wasn't, the returned stream from operator>> would be converted to false
  // and the loop wouldn't even be entered
  // do stuff with correctly initialized data (hopefully)
}

在你的第二个问题上:因为
if(scanf("...",...)!=EOF)

是相同的。
if(!(inStream >> data).eof())

不同于

if(!inStream.eof())
    inFile >> data

14
值得一提的是,如果 (!(inStream >> data).eof()) 也不会产生任何有用的效果。谬误1:如果最后一个数据后面没有空白字符,则条件不会进入(最后一个数据将不被处理)。谬误2:即使读取数据失败,只要没有达到EOF,它也会进入条件(导致无限循环,一遍又一遍地处理相同的旧数据)。 - Tronic
5
值得指出的是,这个答案有点误导人。当提取intstd::string或类似的内容时,在提取到结束符之前的最后一个内容并且提取操作到达了结尾时,EOF标志位会被设置,你无需再次读取。之所以在从文件中读取时不会设置该标志是因为在结尾处有一个额外的\n字符。我已经在另一个答案中详细讨论了这个问题。读取char是另一回事,因为它每次只提取一个字符,并且不会一直提取到结尾。 - Joseph Mansfield
91
主要问题在于,“仅仅因为我们没有到达文件结尾(EOF),并不意味着下一次读取就一定会成功。” - Joseph Mansfield
2
@TonyD 完全同意。我之所以这么说是因为我认为大多数人在阅读此类答案时会认为,如果流包含 "Hello"(没有尾随空格或 \n),并且提取了一个 std::string,它将从 Ho 提取字母,停止提取,然后 设置 EOF 位。实际上,它会设置 EOF 位,因为正是 EOF 停止了提取。只是希望为人们澄清这一点。 - Joseph Mansfield
2
// 对(现在未初始化的)数据进行操作 自C++11起,这已不再是正确的做法,请参见https://dev59.com/RGYr5IYBdhLWcg3wy9SA#13379073 - Baum mit Augen
显示剩余10条评论

114

底线优先:通过正确处理空格,以下是如何使用eof(甚至可以比fail()更可靠地进行错误检查)的方法:

while( !(in>>std::ws).eof() ) {
   int data;
   in >> data;
   if ( in.fail() ) /* Handle with 'break' or 'throw' */;
   // Now use data
}

(感谢Tony D的建议突出回答。请参见他下面的评论,了解为什么这更加健壮。)


反对使用eof()的主要论点似乎缺少有关空格作用的重要细微差别。我的观点是,明确检查eof()不仅不总是错误的 - 这似乎是在此类Stack Overflow问题中的一个压倒性意见 - 而且通过正确处理空格,它提供了更清洁和可靠的错误处理,并且是始终正确的解决方案(尽管不一定是最简洁的解决方案)。

总结所建议的“适当”终止和读取顺序如下:

int data;
while(in >> data) {  /* ... */ }

// Which is equivalent to
while( !(in >> data).fail() )  {  /* ... */ }

由于超出文件末尾的读取尝试导致失败,这被视为终止条件。这意味着没有简单的方法来区分成功的流和真正因为eof以外的原因而失败的流。考虑以下流:

  • 1 2 3 4 5<eof>
  • 1 2 a 3 4 5<eof>
  • a<eof>

while(in>>data) 对于所有三个输入都会以设置failbit的方式终止。在第一和第三个输入中,还会设置eofbit。因此,在循环之后,需要非常丑陋的额外逻辑来区分正确的输入(第一个)和不正确的输入(第二个和第三个)。

相比之下,看以下内容:

while( !in.eof() )
{
   int data;
   in >> data;
   if ( in.fail() ) /* Handle with break or throw */;
   // Now use data
}

在这里,in.fail() 验证只要还有可读的内容,并且是正确的格式。它的目的不仅是一个简单的while循环终止符。

到目前为止一切顺利,但是如果流中存在尾随空格——这听起来像是使用eof()作为终止符的主要问题——会发生什么?

我们不需要放弃错误处理;只需吃掉空白即可:

while( !in.eof() )
{
   int data;
   in >> data >> ws; // Eat white space with 'std::ws'
   if ( in.fail() ) /* Handle with 'break' or 'throw' */;
   // Now use data
}

std::ws跳过流中任何可能的(零个或多个)尾随空格,同时设置eofbit,而不是failbit。 因此,只要有至少一个数据可读取,in.fail()就会按预期工作。如果全空格流也可以接受,则正确的形式是:

while( !(in>>ws).eof() )
{
   int data;
   in >> data;
   if ( in.fail() ) /* Handle with 'break' or 'throw' */;
   /* This will never fire if the eof is reached cleanly */
   // Now use data
}

简介: 正确构造的 while(!eof) 不仅是可行的且不是错误的,它允许在范围内定位数据,并提供了错误检查与正常业务逻辑的清晰分离。尽管如此,while(!fail) 无疑是更常见和简洁的习惯用法,在简单情况下(每次读取一个数据类型)可能更受欢迎。


8
“遍历完循环后,就没有简单的方法可以区分正确的输入和不正确的输入。” 除了其中一种情况下 eofbitfailbit 都被设置之外,在另一种情况下只有 failbit 被设置。在循环终止后,只需要检查一次这个情况,而不是在每次迭代之后都检查它。 它只会离开循环一次,因此您只需要检查它为什么仅离开循环一次。while (in >> data) 对所有空流都有效。 - Jonathan Wakely
3
您所说的(之前已经提到的观点)是,一个格式不正确的流可以在循环后通过!eof & fail来识别。但有些情况下不能依赖这种方法,详见上面的评论(http://goo.gl/9mXYX)。无论如何,我并不是建议将`eof`检查作为*始终更好*的选择。我只是在说,它是一种可能的且(在某些情况下更为合适的)方法,而不是像在 SO 中经常声称的“几乎肯定是错的!” - sly
2
举个例子,考虑如何检查数据为一次读取多个字段的重载operator>>的结构体错误。更简单的情况是stream >> my_int,其中流包含例如“-”:设置了eofbit和failbit。这比operator>>的场景更糟糕,因为用户提供的重载至少可以在返回之前清除eofbit来支持while(s >> x)用法。更普遍的情况是,此答案需要清理 - 仅最后的while( !(in>>ws).eof() )通常是可靠的,并且它被埋藏在最后。 - Tony Delroy
只有你提供的“三个例子”中的第一个被设置为结束。第三个没有,因为a无法转换为整数并且未被读取--它将留在输入中。 - Chris Dodd
那么,这就是重点:在遇到坏的流时尽早中断,或者在读取到文件结尾时成功结束循环。 - sly

79

因为如果程序员不写 while(stream >> n),他们可能会写这样的代码:

while(!stream.eof())
{
    stream >> n;
    //some work on n;
}

问题在于,如果在检查流读取是否成功之前不先对 n 进行一些处理,那么如果读取失败,你的处理结果可能会出现意外。

关键在于,eofbitbadbit 或者 failbit在尝试从流中读取数据后设置的。 因此,如果 stream >> n 失败,那么 eofbitbadbit 或者 failbit 会立即被设置。因此,如果您写成 while (stream >> n),更符合习惯。因为返回的对象 stream 会在从流中读取失败时转换为 false,从而停止循环。如果读取成功,则转换为 true 并继续循环。


2
除了在未定义的值 n 上进行工作会产生“不良结果”之外,如果失败的流操作没有消耗任何输入,程序也可能陷入无限循环 - mastov

15
其他回答已经解释了为什么在while (!stream.eof())中的逻辑是错误的以及如何修复它。我想专注于不同的问题:
引用部分:

为什么仅使用显式检查文件结束是错误的?

一般来说,仅检查eof是错误的,因为流提取(>>)在未到达文件结尾的情况下可能失败。如果您有例如int n; cin >> n;并且流包含hello,那么h不是有效数字,因此提取将在未达到输入结尾时失败。
这个问题,加上在尝试从中读取数据之前检查流状态的一般逻辑错误,意味着对于N个输入项,循环将运行N+1次,导致以下症状:
  • 如果流为空,则循环将运行一次。>>将失败(没有输入可读取),所有应该被设置的变量(通过stream >> x)实际上都没有被初始化。这会导致处理垃圾数据,可能表现为无意义的结果(通常是巨大的数字)。

    (如果您的标准库符合C++11,现在情况有点不同:失败的>>现在将数字变量设置为0,而不是保留它们未初始化(除了char)。)

  • 如果流不为空,则在最后一个有效输入后,循环将再次运行。由于在最后一次迭代中所有的>>操作都失败了,变量很可能会保持其来自上一次迭代的值。这可能表现为“最后一行被打印两次”或“最后一个输入记录被处理两次”。

    (自C++11以来,这应该表现得有些不同(见上文):现在您会得到一个由零组成的“幻像记录”,而不是重复的最后一行。)

  • 如果流包含格式不正确的数据,但只检查.eof,则会陷入无限循环。>>将无法从流中提取任何数据,因此循环会无限地旋转而永远不会到达结尾。


总之:解决方案是测试>>操作本身的成功,而不是使用单独的.eof()方法:while (stream >> n >> m) { ... },就像在C语言中测试scanf调用本身的成功一样:while (scanf("%d%d", &n, &m) == 2) { ... }


1
这是最准确的答案,尽管从C++11开始,我不认为变量还会未初始化(第一个项目符号)。 - csguy

4

重要的是要记住,inFile.eof()直到尝试读取失败后才变为True,因为你已经到达文件末尾。所以,在这个例子中,你会出现错误。

while (!inFile.eof()){
    inFile >> x;
        process(x);
}

正确的循环方式是将读取和检查结合成一个操作,如下所示。
while (inFile >> x) 
    process(x)

按照惯例,operator>> 返回我们读取的流以及流上的布尔测试。当流失败时(例如到达文件末尾)布尔测试返回 False

因此,这给我们提供了正确的顺序:

  • 读取
  • 检查读取是否成功
  • 仅在检查成功的情况下处理我们读取的内容

如果你遇到了其他一些阻止你正确读取文件的问题,你将无法像遇到文件结束符那样到达eof()。例如,让我们看看下面的示例。

int x; 
while (!inFile.eof()) { 
    inFile >> x; 
    process(x);
} 

让我们通过一个例子来追踪上面代码的工作过程:
假设文件内容是'1', '2', '3', 'a', 'b'。循环将正确读取1、2和3。然后它将到达'a'。当它尝试将'a'提取为int时,会失败。流现在处于失败状态,直到我们清除流,否则所有尝试从中读取的操作都将失败。但是,当我们测试eof()时,它将返回False,因为我们还没有到文件结尾,因为仍有'a'等待读取。循环将继续尝试从文件中读取,每次都会失败,因此它永远无法到达文件结尾。因此,上面的循环将永远运行。但是,如果我们使用这样的循环,我们将得到所需的输出。
while (inFile >> x)
    process(x);

在这种情况下,流将在文件结束时以及失败转换的情况下(比如我们无法将a读取为整数)转换为False。

1
语法错误:未定义标识符“True”和“False”。 - MatG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接