为什么 iostream::eof 在循环条件(即 'while (!stream.eof())'))中被认为是错误的?

Why is iostream::eof inside a loop condition (i.e. `while (!stream.eof())`) considered wrong?

提问人:MAK 提问时间:4/9/2011 最后编辑:melpomeneMAK 更新时间:1/30/2023 访问量:96353

问:

我刚刚在这个答案中发现了一条评论,说在循环条件下使用“几乎肯定是错误的”。我通常使用类似的东西 - 我猜它隐式检查 EOF。iostream::eofwhile(cin>>n)

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

它与在 C 中使用(我经常毫无问题地使用)有何不同?scanf("...",...)!=EOF

C++ IOSTREAM C++-常见问题解答

评论

27赞 Ben Voigt 4/6/2012
scanf(...) != EOF在 C 中也不起作用,因为返回成功解析和分配的字段数。正确的条件是 where 是格式字符串中的字段数。scanfscanf(...) < nn
8赞 Sebastian 11/24/2012
@Ben Voigt,如果达到 EOF,它将返回一个负数(EOF 通常被定义为负数)
23赞 Ben Voigt 11/24/2012
@SebastianGodelet:实际上,如果在第一次字段转换(成功与否)之前遇到文件末尾,它将返回。如果字段之间达到文件末尾,它将返回成功转换和存储的字段数。这使得比较是错误的。EOFEOF
2赞 Ben Voigt 11/25/2012
@SebastianGodelet:不,不是真的。当他说“通过循环时,没有(简单的)方法来区分正确的输入和不正确的输入”时,他犯了错误。事实上,这就像在循环退出后检查一样简单。.eof()
4赞 sly 11/25/2012
@Ben 是的,对于这种情况(读取一个简单的 int)。但是可以很容易地想出一个场景,即循环以实际故障和 EOF 终止。想想看,如果每次迭代需要 3 个整数(假设您正在读取一个 x-y-z 点或其他东西),但错误地,流中只有两个整数。while(fail)

答:

612赞 Xeo 4/9/2011 #1

因为只有在读取流的末尾才会返回。它并不表示下一次读取将是流的末尾。iostream::eoftrue

考虑一下(并假设下一次读取将在流的末尾):

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赞 Tronic 1/21/2013
值得一提的是,如果(!(inStream >> data).eof()) 也没有做任何有用的事情。谬误 1:如果最后一条数据之后没有空格,则不会进入条件(不会处理最后一个数据)。谬误2:只要没有达到EOF,即使读取数据失败,它也会进入条件(无限循环,一遍又一遍地处理相同的旧数据)。
5赞 Joseph Mansfield 4/7/2013
我认为值得指出的是,这个答案略有误导。提取 s 或 s 或类似内容时,当您在末尾之前提取 EOF 位并且提取到达末尾时,设置 EOF 位。您不需要再次阅读。从文件中读取时没有设置它的原因是最后有一个额外的。我已经在另一个答案中谈到了这一点。读取 s 是另一回事,因为它一次只提取一个,并且不会继续到达末尾。intstd::string\nchar
91赞 Joseph Mansfield 4/7/2013
主要问题是,仅仅因为我们还没有达到EOF,并不意味着下一次读取会成功
2赞 Joseph Mansfield 4/23/2013
@TonyD 完全同意。我之所以这么说,是因为我认为大多数人在阅读这个和类似的答案时会认为,如果流包含(没有尾随空格或)并且提取了 a,它将从 to 中提取字母,停止提取,然后设置 EOF 位。事实上,它会设置 EOF 位,因为是 EOF 停止了提取。只是希望为人们澄清这一点。"Hello"\nstd::stringHo
2赞 Baum mit Augen 8/22/2016
// do stuff with (now uninitialized) data自 C++11 起,情况不再如此,请参阅 stackoverflow.com/a/13379073/3002139
79赞 Nawaz 4/9/2011 #2

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

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

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

关键是,在尝试从流中读取数据后设置、、或。因此,如果失败,则立即设置,或,因此,如果您编写,则更习惯使用,因为返回的对象将转换为从流中读取时出现一些故障,因此循环停止。如果读取成功并且循环继续,它将转换为。eofbitbadbitfailbitstream >> neofbitbadbitfailbitwhile (stream >> n)streamfalsetrue

评论

2赞 mastov 4/27/2018
除了在未定义值上执行操作时提到的“意外结果”之外,如果失败的流操作不消耗任何输入,程序也可能陷入无限循环n
115赞 sly 11/24/2012 #3

底线顶部:通过正确处理空格,以下是如何使用(甚至比错误检查更可靠):eoffail()

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

(感谢 Tony D 提出的建议,以突出答案。请参阅下面的评论,了解为什么这更健壮。)


反对使用的主要论点似乎忽略了关于空白作用的重要微妙之处。我的主张是,显式检查不仅不是“总是错误的”——这似乎是这个和类似的 Stack Overflow 问题中最重要的观点——而且通过正确处理空格,它提供了更干净、更可靠的错误处理,并且是始终正确的解决方案(尽管不一定是最简洁的)。eof()eof()

总结一下建议的“正确”终止和读取顺序如下:

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

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

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

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

while(in>>data)所有三个输入的集合结束。在第一个和第三个,也被设置了。因此,通过循环,需要非常丑陋的额外逻辑来区分正确的输入(第一个)和不正确的输入(第二个和第三个)。failbiteofbit

然而,采取以下措施:

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

在这里,验证只要有东西要读,它就是正确的。它的目的不仅仅是一个while循环终结器。in.fail()

到目前为止一切顺利,但是如果流中有尾随空间会发生什么——这听起来像是终结者的主要问题?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跳过流中任何潜在的(零个或多个)尾随空间,同时设置 ,而不是 failbit。因此,只要至少有一个数据要读取,就可以按预期工作。如果全空白流也可以接受,那么正确的形式是:eofbitin.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赞 Jonathan Wakely 2/25/2013
"因此,通过循环,没有(简单的)方法来区分正确的输入和不正确的输入。除了在一种情况下两者都被设置,在另一种情况下只有被设置。你只需要在循环终止后测试一次,而不是在每次迭代时测试;它只会离开循环一次,所以你只需要检查它离开循环一次的原因。 适用于所有空白流。eofbitfailbitfailbitwhile (in >> data)
3赞 sly 2/25/2013
您所说的(以及之前提出的观点)是可以将格式错误的流识别为过去的循环。在某些情况下,人们不能依赖这一点。见上文评论(goo.gl/9mXYX)。无论哪种方式,我都不建议将检查作为总是更好的选择。我只是说,这是一种可能的(在某些情况下更合适的)方法来做到这一点,而不是像 SO 中倾向于声称的那样“肯定是错误的!!eof & faileof
2赞 Tony Delroy 2/25/2015
“举个例子,考虑一下你如何检查错误,其中数据是一个带有重载运算符的结构>>一次读取多个字段”——支持你的观点的一个更简单的情况是流包含例如“-”:和设置。这比用户提供的重载至少可以选择在返回之前清除以帮助支持使用的情况更糟糕。更一般地说,这个答案可以使用清理 - 只有最终通常是健壮的,并且在最后被埋葬。stream >> my_inteofbitfailbitoperator>>eofbitwhile (s >> x)while( !(in>>ws).eof() )
0赞 Chris Dodd 4/18/2022
使用您的“3 个示例”,只有第一组 eof。第三个不会,因为无法转换为 int 并且没有被读取 -- 它将留在输入上。a
0赞 sly 4/26/2022
所以?这就是重点:用一个坏的流提前中断,或者用EOF成功地结束循环。
15赞 melpomene 5/4/2019 #4

其他答案已经解释了为什么逻辑是错误的以及如何解决它。我想专注于一些不同的东西:while (!stream.eof())

为什么明确使用错误的检查 EOF?iostream::eof

一般而言,检查 only 是错误的,因为流提取 () 可能会在不到达文件末尾的情况下失败。如果您有例如 并且流包含 ,则不是有效数字,因此提取将失败,而不会到达输入的末尾。eof>>int n; cin >> n;helloh

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

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

    (如果你的标准库符合 C++11,现在情况有点不同:失败现在将数值变量设置为,而不是使它们处于未初始化状态(s 除外)。>>0char

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

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

  • 如果流包含格式不正确的数据,但您只检查 ,则最终会得到无限循环。 将无法从流中提取任何数据,因此循环在原地旋转而从未到达终点。.eof>>


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

评论

1赞 csguy 8/20/2019
这是最准确的答案,尽管从 C++11 开始,我不相信变量不再未初始化(第一个项目符号 PT)
5赞 Deepthi Tabitha Bennet 2/20/2022 #5

要记住的重要一点是,直到尝试读取失败才会出现这种情况,因为您已经到达了文件的末尾。因此,在此示例中,您将收到一个错误。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
  • 当它尝试提取为 int 时,它将失败。a
  • 流现在处于失败状态,直到或除非我们流,所有从中读取的尝试都将失败。clear
  • 但是,当我们测试 eof() 时,它会返回 ,因为我们不在文件的末尾,因为还有等待读取。Falsea
  • 循环将不断尝试从文件中读取,并且每次都失败,因此它永远不会到达文件末尾。
  • 因此,上面的循环将永远运行。

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

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

在这种情况不仅会在文件结束时转换为,而且在转换失败的情况下也会转换为,例如我们无法读取为整数。Falsea

评论

1赞 MatG 5/8/2022
?语法错误: 未定义的标识符和TrueFalse