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)。

我们可以通过使用 a 而不是文件的简单示例看到这一点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()其用作提取条件的真正原因是什么?

4

2 回答 2

19

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

hello
world

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

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

hello\nworld\n

这就是在!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 和失败位。


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

于 2013-01-30T23:15:59.007 回答
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()

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

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

原因如上所述......文件往往被一个'\ n'字符终止,当它们是getline或>> std::string返回最后一个非空白标记时,不需要“尝试提取文件结尾” (使用你的短语)。

于 2013-04-23T03:25:02.787 回答