为啥循环条件内的 iostream::eof 被认为是错误的(即 `while (!stream.eof())`)?

Posted

技术标签:

【中文标题】为啥循环条件内的 iostream::eof 被认为是错误的(即 `while (!stream.eof())`)?【英文标题】:Why is iostream::eof inside a loop condition (i.e. `while (!stream.eof())`) considered wrong?为什么循环条件内的 iostream::eof 被认为是错误的(即 `while (!stream.eof())`)? 【发布时间】:2011-04-09 12:49:36 【问题描述】:

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

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

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

【问题讨论】:

scanf(...) != EOF 在 C 中也不起作用,因为 scanf 返回成功解析和分配的字段数。正确的条件是scanf(...) < n,其中n 是格式字符串中的字段数。 @Ben Voigt,如果达到 EOF,它将返回一个负数(通常定义为 EOF) @SebastianGodelet:实际上,如果在第一次字段转换(成功与否)之前遇到文件结尾,它将返回EOF。如果字段之间到达文件结尾,它将返回成功转换和存储的字段数。这使得与EOF 的比较是错误的。 @SebastianGodelet:不,不是真的。当他说“通过循环,没有(简单的)方法可以区分正确的输入和不正确的输入”时,他犯了错误。事实上,这就像在循环退出后检查.eof() 一样简单。 @Ben 是的,对于这种情况(读取一个简单的 int)。但是可以很容易地想出一个场景,while(fail) 循环以实际失败和 eof 终止。考虑一下每次迭代是否需要 3 个整数(例如,您正在读取 x-y-z 点或其他内容),但错误地,流中只有两个整数。 【参考方案1】:

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

【讨论】:

值得一提的是 if (!(inStream >> data).eof()) 也没有做任何有用的事情。谬误一:如果最后一条数据后面没有空格,则不进入条件(不处理最后一条数据)。谬误2:即使读取数据失败也会进入条件,只要没有达到EOF(无限循环,一遍又一遍地处理相同的旧数据)。 我认为值得指出的是,这个答案有点误导。当提取ints 或std::strings 或类似的,EOF 位 is 当您在结束之前提取一个并且提取命中结束时设置。您无需再次阅读。从文件读取时没有设置它的原因是因为最后有一个额外的\n。我已经在another answer 中介绍了这一点。阅读chars 是另一回事,因为它一次只提取一个,并且不会继续到达终点。 主要问题是仅仅因为我们没有到达EOF,并不意味着下一次读取会成功 @sftrabbit: all tr​​ue 但不是很有用......即使没有尾随'\n',希望其他尾随空格与整个文件中的其他空格一致处理是合理的(即跳过) .此外,“当你之前提取一个”的一个微妙后果是,当输入完全为空时,while (!eof()) 不会在ints 或std::strings 上“工作”,所以即使知道没有尾随 @ 987654336@ 需要护理。 @TonyD 完全同意。我之所以这么说是因为我认为大多数人在阅读本文和类似答案时会认为,如果流包含"Hello"(没有尾随空格或\n)并且提取了std::string,它将提取从Ho 的字母,停止提取,然后not 设置EOF 位。事实上,它会设置 EOF 位,因为正是 EOF 停止了提取。只是希望为人们澄清这一点。【参考方案2】:

底线顶部:通过正确处理空白,以下是如何使用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() 不仅不是“总是错误”——这似乎是这个和类似的 SO 线程中的一个压倒一切的观点——而且正确处理了白色——空间,它提供了更清晰和更可靠的错误处理,并且是始终正确的解决方案(尽管不一定是最简单的)。

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

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) 以一组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) 无疑是一个更常见和更简洁的习惯用法,并且在简单(每个读取类型的单个数据)场景中可能是首选。

【讨论】:

"所以在循环之后没有(简单的)方法可以区分正确的输入和不正确的输入。" 除了在一种情况下eofbit 和 @987654346 @ 被设置,在其他只有failbit 被设置。您只需要在循环终止后测试一次,而不是每次迭代;它只会离开循环一次,所以你只需要检查 why 它离开循环一次。 while (in >> data) 适用于所有空白流。 您所说的(以及前面提到的一点)是格式错误的流可以被识别为!eof & fail 过去的循环。在某些情况下,不能依赖这一点。请参阅上面的评论 (goo.gl/9mXYX)。不管怎样,我不建议将eof-check 作为 the-always-better 替代方案。我只是说,它一种可能且(在某些情况下更合适)的方式,而不是“肯定是错误的!”因为它往往在 SO 中被声明。 “例如,考虑如何检查数据是具有重载运算符的结构的错误>>一次读取多个字段” - 一个更简单的案例支持您的观点的是stream >> my_int,其中流包含例如“-”:eofbitfailbit 已设置。这比operator>> 场景更糟糕,其中用户提供的过载至少可以选择在返回帮助支持while (s >> x) 使用之前清除eofbit。更一般地说,这个答案可以使用清理 - 只有最后的 while( !(in>>ws).eof() ) 通常是健壮的,它在最后被埋没了。【参考方案3】:

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

while(!stream.eof())

    stream >> n;
    //some work on n;

这里的问题是,你不能在不首先检查流读取是否成功的情况下执行some work on n,因为如果不成功,你的some work on n 会产生不希望的结果。

重点在于,eofbitbadbitfailbit 是在尝试从流中读取之后设置的。因此,如果 stream >> n 失败,则 @ 987654329@、badbitfailbit 会立即设置,因此如果您编写 while (stream >> n),它会更惯用,因为如果从流中读取失败,则返回的对象 stream 将转换为 false,因此循环停止。如果读取成功并且循环继续,它将转换为true

【讨论】:

除了在n的未定义值上工作时提到的“不良结果”,如果流操作失败,程序也可能陷入无限循环不消耗任何输入。【参考方案4】:

其他答案已经解释了为什么while (!stream.eof()) 中的逻辑错误以及如何修复它。我想专注于不同的事情:

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

一般来说,检查eof 是错误的,因为流提取 (>>) 可能会失败而不会到达文件末尾。如果你有例如int n; cin >> n; 并且流包含hello,则h 不是有效数字,因此在未到达输入末尾时提取将失败。

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

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

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

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

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

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


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

【讨论】:

这是最准确的答案,尽管从 c++11 开始,我不再相信变量未初始化(第一个项目符号 pt)【参考方案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。 当它尝试将 a 提取为 int 时,它会失败。 流现在处于失败状态,直到或除非我们clear 流,所有读取它的尝试都将失败。 但是,当我们测试 eof() 时,它会返回 False,因为我们不在文件末尾,因为还有 a 等待读取。 循环会不断尝试从文件中读取,并且每次都失败,因此它永远不会到达文件末尾。 所以,上面的循环将永远运行。

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

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

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

【讨论】:

【参考方案6】:

循环中的 iostream::eof 被认为是错误的,因为我们还没有到达 EOF。所以并不代表下次读取就成功了。

我将通过两个示例代码来解释我的陈述,这肯定会帮助您以更好的方式理解这个概念。比方说,当我们想在 C++ 中使用文件流读取文件时。而当我们使用循环写入文件时,如果我们使用stream.eof()检查文件的结尾,我们实际上是在检查文件是否已经到达结尾。

示例代码

#include<iostream>
#include<fstream>
using namespace std;
int main() 
   ifstream myFile("myfile.txt");
   string x;
   while(!myFile.eof()) 
      myFile >> x;
     // Need to check again if x is valid or eof
     if(x) 
        // Do something with x
     
   

当我们在循环中直接使用流时,我们不会再次检查条件。

示例代码

#include<iostream>
#include<fstream>
using namespace std;
int main() 
   ifstream myFile("myfile.txt");
   string x;
   while(myFile >> x) 
      // Do something with x
      // No checks needed!
   

【讨论】:

以上是关于为啥循环条件内的 iostream::eof 被认为是错误的(即 `while (!stream.eof())`)?的主要内容,如果未能解决你的问题,请参考以下文章

在此代码中,对于 while 循环内的条件,错误显示“除以零”。为啥会这样?

为啥 comb 在循环内的行为不同?

如何从while循环内的if条件中断while循环?

在条件(如果)内的要求(外部文件)中定义的 PHP7 const 正在工作 - 为啥?

leetcode lowest common ancestor of a binary search tree为啥不能直接用while

通过循环内的条件更改附加的 td