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

Jos*_*eld 10 c++ iostream eof language-lawyer

灵感来自我之前的问题

新C++程序员的一个常见错误是从文件中读取以下内容:

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

他们经常会报告文件的最后一行被读取两次.这个问题的常见解释(我之前给出的一个)类似于:

如果您尝试提取文件结尾,则提取将仅在流上设置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;
}
Run Code Online (Sandbox Code Playgroud)

很明显,这里的单次提取获得hello从字符串设置EOF位为1.

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

Jos*_*eld 18

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

hello
world
Run Code Online (Sandbox Code Playgroud)

第二次提取会吃掉world,停在文件末尾,然后设置EOF位.下一次迭代不会发生.

但是,许多文本编辑都有一个肮脏的秘密.当你保存文本文件时,他们会骗你,就像那样简单.他们没有告诉你的是\n文件末尾有一个隐藏的内容.文件中的每一行都以a结尾\n,包括最后一行.所以文件实际上包含:

hello\nworld\n
Run Code Online (Sandbox Code Playgroud)

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

你没有经历过这个,std::stringstream因为你在流中坚持的就是你得到的.与文件不同,\n最后没有std::stringstream ss("hello").如果你这样做std::stringstream ss("hello\n"),你会遇到相同的重复行问题.

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

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

使用while (std::getline(...))和相关条件完美地工作,因为在提取开始之前,格式化的输入函数检查是否设置了任何坏,失败或EOF位.如果它们中的任何一个,它会立即结束,在此过程中设置失败位.如果它在找到它想要提取的内容之前找到文件结尾,同时设置eof和fail位,它也将失败.


注意:\n如果您在保存之前:set noeol:set binary保存之前,可以在vim中保存没有额外文件的文件.

  • @DanielFischer,将新行从文件的最后一行留下来触发的错误就像因为存在错误而引发的错误一样多.正确的解决方案是编写以任何方式工作的程序. (6认同)
  • @PeteBecker,你有参考支持吗?由于很难直观地看到最后一行是EOL终止,这样的规则会不必要地苛刻 - 你只是在寻找bug. (3认同)
  • 在文本模式下读取文件的末尾需要一个新行. (2认同)
  • @MarkRansom - 它是古老的C,用于与必须在面向记录的I/O之上强制流的大型机兼容. (2认同)