不使用 EOF 位作为流提取条件的真正原因是什么?

What's the real reason to not use the EOF bit as our stream extraction condition?

本文关键字:是什么 条件 提取 EOF      更新时间:2023-10-16

灵感来自我之前的问题

对于新C++程序员来说,一个常见的错误是从包含以下内容的文件中读取:

std::ifstream file("foo.txt");
std::string line;
while (!file.eof()) {
  file >> line;
  // Do something with line
}

他们通常会报告文件的最后一行被读取了两次。这个问题的常见解释(我之前给出的解释)是这样的:

仅当您尝试提取文件末尾时,提取才会在流上设置 EOF 位,而不是在提取仅在文件末尾停止时设置。 file.eof()只会告诉您上一次读取是否到达文件末尾,而不是下一次读取是否到达文件末尾。提取最后一行后,EOF 位仍未设置,并且再次进行迭代。但是,在最后一次迭代中,提取失败,line仍然具有与以前相同的内容,即最后一行是重复的。

但是,此解释的第一句话是错误的,因此对代码正在做什么的解释也是错误的。

格式化输入函数的定义(operator>>(std::string&))将提取定义为使用rdbuf()->sbumpc()rdbuf()->sgetc()来获取输入字符。它指出,如果这些函数中的任何一个返回 traits::eof() ,则设置 EOF 位:

如果 rdbuf()->sbumpc()rdbuf()->sgetc() 返回 traits::eof() ,则输入函数(除非另有明确说明)在返回之前完成其操作并执行setstate(eofbit),这可能会抛出ios_base::failure (27.5.5.4)。

我们可以通过使用std::stringstream而不是文件的简单示例看到这一点(它们都是输入流,并且在提取时的行为方式相同):

int main(int argc, const char* argv[])
{
  std::stringstream ss("hello");
  std::string result;
  ss >> result;
  std::cout << ss.eof() << std::endl; // Outputs 1
  return 0;
}

这里很明显,单个提取从字符串中获取hello并将 EOF 位设置为 1。

那么解释有什么问题呢?导致!file.eof()导致最后一行重复的文件有什么不同?我们不应该使用!file.eof()作为提取条件的真正原因是什么?

是的,如果提取在文件末尾停止,则从输入流中提取将设置 EOF 位,如std::stringstream示例所示。如果这么简单,以 !file.eof() 作为其条件的循环在以下文件上可以正常工作:

hello
world

第二次提取会吃world,在文件末尾停止,从而设置 EOF 位。下一次迭代不会发生。

但是,许多文本编辑器都有一个肮脏的秘密。当您保存文本文件时,即使这么简单,他们也在骗你。他们没有告诉您的是文件末尾有一个隐藏的n。文件中的每一行都以n结尾,包括最后一行。因此,该文件实际上包含:

hellonworldn

这就是使用!file.eof()作为条件时导致最后一行重复的原因。现在我们知道了这一点,我们可以看到第二次提取将吃掉world停止在n并且设置 EOF 位(因为我们还没有到达那里)。循环将第三次迭代,但下一次提取将失败,因为它找不到要提取的字符串,只有空格。字符串保留其先前的值仍然悬而未决,因此我们得到重复的行。

你不会在std::stringstream身上体验到这一点,因为你粘在流中的东西正是你得到的。与文件中不同,std::stringstream ss("hello")末尾没有n。如果您要执行std::stringstream ss("hellon"),则会遇到相同的重复行问题。

所以当然,我们可以看到,从文本文件中提取时,我们永远不应该使用!file.eof()作为条件 - 但这里真正的问题是什么?为什么我们真的不应该使用它作为我们的条件,无论我们是否从文件中提取?

真正的问题是,eof()让我们不知道下一次读取是否会失败。在上面的例子中,我们看到即使eof()为 0,下一次提取也会失败,因为没有要提取的字符串。如果我们没有将文件流与任何文件关联,或者流为空,也会发生同样的情况。不会设置 EOF 位,但没有什么可读取的。我们不能仅仅因为未设置eof()就盲目地继续从文件中提取。

使用while (std::getline(...))和相关条件非常有效,因为在提取开始之前,格式化的输入函数会检查是否设置了任何错误、失败或 EOF 位。如果其中任何一个是,它将立即结束,设置进程中的故障位。如果在找到要提取的内容之前找到文件末尾,同时设置 eof 和失败位,它也将失败。


注意:如果您在保存之前执行:set noeol:set binary,则可以在 vim 中保存文件而不带额外n

你的问题有一些虚假的概念。 你给出一个解释:

"只有在您尝试提取文件末尾时,提取才会在流上设置 EOF 位,而不是在提取只是在文件末尾停止时。"

然后声称它"是错误的,因此对代码所做的事情的解释也是错误的。

其实没错。 让我们看一个例子。

当读到std::string...

std::istringsteam iss('abcn');
std::string my_string;
iss >> my_string;

。默认情况下,就像您的问题一样,operator>>正在读取字符,直到找到空格 EOF。 所以:

  • 'abcn' 读取 -> 一旦遇到'n',它不会"尝试提取文件末尾",而是"只是在 [EOF] 处停止",eof()不会返回true
  • 'abc'读取 而不是 -> 它是尝试提取文件结尾,发现string内容的结尾,因此eof()将返回true

同样,将'123'解析为int会设置eof(),因为解析不知道是否会有另一个数字并尝试继续读取它们,点击eof()。 将'123 '解析为int不会设置eof()

至关重要的是,将"a"解析为char不会设置eof()因为不需要尾随空格来知道解析是完整的 - 一旦读取了一个字符,就不会尝试查找另一个字符,也不会遇到eof()。 (当然,从同一流进一步解析命中eof)。

很明显 [对于字符串流 "hello">> std::string] 单个提取从字符串中获取 hello 并将 EOF 位设置为 1。 那么解释有什么问题呢?导致 !file.eof() 导致最后一行重复的文件有什么不同?我们不应该使用 !file.eof() 作为提取条件的真正原因是什么?

原因如上...该文件往往由""字符终止,当它们被终止时,意味着 getline 或 >> std::string返回最后一个非空格标记,而无需"尝试提取文件末尾"(使用您的短语)。