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

10

受到我之前的问题启发

新手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()作为我们的提取条件的真正原因是什么?


新手C++程序员常犯的错误是阅读劣质教材。 - Cubbi
一个常见的错误是没有检查每个流操作:if(!(stream>>var)) { doErrorHandling(); } - CoffeDeveloper
1
@GameDeveloper 这太过了。如果我快速连续读取五个变量,并且只关心它们是否全部成功,那么我只需要在最后检查stream即可。在那里进行五次单独的检查只会让事情变得混乱。 - Lightness Races in Orbit
2个回答

20

是的,从输入流中提取数据时,如果提取到文件末尾会设置EOF(End-Of-File)比特位,就像std::stringstream示例所演示的一样。如果真的这么简单,以!file.eof()为条件的循环将在如下文件上正常工作:

hello
world
第二个提取将读取"world",并在文件结束时停止,从而设置EOF位。下一个迭代将不会发生。
然而,许多文本编辑器有一个不可告人的秘密。即使是保存一个简单的文本文件,它们也会在欺骗你。它们没有告诉你文件末尾隐藏了一个'\n'。文件中每一行都以'\n'结尾,包括最后一行。因此,该文件实际上包含:
hello\nworld\n
这就是为什么在使用 !file.eof() 作为条件时会导致最后一行重复的原因。现在我们知道了,可以看到第二次提取将吃掉 world 停在\n处,并且没有设置EOF位(因为我们还没有到达那里)。循环将迭代第三次,但下一个提取将失败,因为它找不到要提取的字符串,只有空格。该字符串仍保留其先前的值,因此我们得到了重复的行。
您不会在 std::stringstream 中遇到这种情况,因为您放入流中的内容就是您得到的内容。与文件不同,在 std::stringstream ss(“hello”)结尾处没有\n。如果您执行 std:: stringstream ss(“hello \n”),则会遇到相同的重复行问题。
所以当然,我们可以看到从文本文件提取时永远不应该使用!file.eof()作为条件,但是真正的问题是什么呢?无论我们是从文件中提取还是从其他地方提取,为什么我们绝对不应该使用它作为条件?
真正的问题是, eof()无法告诉我们下一次读取是否会失败。在上面的情况中,我们看到即使 eof()为0,下一个提取也会失败,因为没有要提取的字符串。如果我们没有将文件流与任何文件关联或者流为空,则会出现相同的情况。EOF位不会被设置但是没有要读取的内容。我们不能仅仅因为未设置EOF位就盲目地从文件中提取。
使用 while(std::getline(...))等条件完美地工作,因为在提取开始之前,格式化输入函数会检查是否有任何坏的、失败的或EOF位被设置。如果任何一个被设置了,它立即结束并在此过程中设置fail位。如果在找到要提取的内容之前找到了文件结尾,它还将失败,并设置eof和fail位。
注意:如果您在保存之前执行:set noeol:set binary,则可以在vim中保存不带额外\n的文件。

1
一个好的编辑器不会自动添加换行符,除非你明确告诉它这样做。 - Daniel Fischer
6
@DanielFischer,如果文件的最后一行没有换行符,则会触发与在最后一行加上换行符一样多的错误。正确的解决方案是编写可以同时适用于两种情况的程序。 - Mark Ransom
2
在文本模式下读取文件时,需要在文件末尾添加一个新行。 - Pete Becker
3
@PeteBecker,你有支持你说法的参考资料吗?因为最后一行是否以EOL结尾在视觉上很难看出来,这样的规则会过于严格——只会引发bug。 - Mark Ransom
2
@MarkRansom - 这是古老的C语言,为了与必须在记录导向的I/O之上强制流的大型机兼容而设计。 - Pete Becker
显示剩余3条评论

4
您的问题存在一些错误的概念。您给出了一个解释:

“如果您尝试提取文件结束符,则提取仅会在流上设置EOF位,而不是如果您的提取仅停止在文件结束处。”

然后宣称它“是错误的,因此代码正在执行的解释也是错误的。”
实际上,这是正确的。让我们看一个例子...
当读取到std::string时...
std::istringsteam iss('abc\n');
std::string my_string;
iss >> my_string;

默认情况下,与您的问题类似,operator>>会读取字符,直到找到空格或EOF。因此:
  • 'abc\n'读取->一旦遇到'\n',它不会“尝试提取文件结尾”,而是“在[EOF]处停止”,eof()不会返回true,
  • 如果改为从'abc'读取->它试图提取文件结尾以发现string内容的结尾,因此eof()将返回true

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

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

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

原因如上所述......文件通常由'\n'字符终止,并且当它们终止时,意味着getline或>> std::string返回最后一个非空格标记,而无需“尝试提取文件结尾”(使用您的措辞)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接