为什么iostream::eof在循环条件(即:' while (!stream.eof()) ')错误吗?

我刚刚在回答中发现了一条评论,说在循环条件中使用iostream::eof“几乎肯定是错误的”。我通常使用像while(cin>>n)这样的东西——我猜它隐式地检查EOF。

为什么检查eof显式使用while (!cin.eof())错误?

它与在C中使用scanf("...",...)!=EOF有什么不同(我经常使用没有问题)?

89395 次浏览

因为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

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

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

这里的问题是,如果不首先检查流读取是否成功,就不能执行some work on n,因为如果不成功,some work on n将产生不希望看到的结果。

重点是,eofbitbadbit,或failbit被设置为badbit1所以如果stream >> n失败,那么eofbitbadbit,或failbit被立即设置,所以如果你写while (stream >> n),它更习惯,因为返回的对象stream转换为false,如果从流中读取失败,因此循环停止。如果读取成功并且循环继续,它将转换为badbit0。

通过正确处理空白,下面是如何使用eof(甚至,在错误检查方面比fail()更可靠):

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

(# EYZ0)


反对使用eof()的主要论点似乎忽略了关于空白的作用的一个重要的微妙之处。我的主张是,显式检查eof()不仅不是“总是错的”——这似乎是在这个和类似的SO线程中最重要的观点——但通过适当处理空白,它提供了一个更清晰和更可靠的错误处理,并且是总是正确的解决方案(尽管不一定是最简洁的)。

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

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


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

读取尝试超过eof导致的失败作为终止条件。这意味着没有简单的方法来区分成功的流和真正因为eof以外的原因而失败的流。以以下流程为例:

  • # EYZ0
  • # EYZ0
  • # EYZ0

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 whitespace 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)无疑是一种更常见、更简洁的习惯用法,可能更适合简单的(每个读取类型只有一个数据)场景。

其他答案解释了为什么while (!stream.eof())中的逻辑是错误的,以及如何修复它。我想关注一些不同的东西:

为什么检查eof显式使用iostream::eof错误?

一般来说,检查eof 只有是错误的,因为流提取(>>)可能在没有到达文件末尾的情况下失败。如果你有int n; cin >> n;,流包含hello,那么h不是一个有效的数字,所以提取将在没有到达输入结束时失败。

此问题与检查流状态之前试图从中读取的一般逻辑错误(这意味着对于N个输入项,循环将运行N+1次)结合在一起,导致以下症状:

  • 如果流为空,循环将运行一次。>>将失败(没有要读取的输入),所有应该设置的变量(由stream >> x设置)实际上是未初始化的。这将导致垃圾数据被处理,这可能表现为无意义的结果(通常是巨大的数字)。

    (如果你的标准库符合c++ 11,事情现在有点不同:失败的>>现在将数值变量设置为0,而不是不初始化它们(除了chars)。)

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

    (这应该与c++ 11有所不同(见上文):现在你得到一个0的“幻影记录”,而不是重复的最后一行。)

  • 如果流包含格式错误的数据,但您只检查.eof,那么您最终会得到一个无限循环。>>将无法从流中提取任何数据,因此循环在没有到达终点的情况下原地旋转。


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

需要记住的重要一点是,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时,它将失败。
  • 流现在处于失败状态,除非我们clear流,否则从流中读取的所有尝试都会失败。
  • 但是,当我们测试eof()时,它将返回False,因为我们不在文件的末尾,因为仍然有a等待读取。
  • 循环将继续尝试从文件中读取,但每次都失败,因此从来没有到达文件的末尾。
  • 因此,上面的循环将永远运行。

但是,如果我们使用这样的循环,我们将得到所需的输出。

while (inFile >> x)
process(x);

在这种情况下,流不仅在文件结束时转换为False,而且在转换失败时也会转换为a,例如我们不能将a读取为整数。