不使用 EOF 位作为流提取条件的真正原因是什么?
What's the real reason to not use the EOF bit as our stream extraction condition?
灵感来自我之前的问题
对于新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
返回最后一个非空格标记,而无需"尝试提取文件末尾"(使用您的短语)。
- 在什么条件下使用 std::memcpy 在对象之间复制是安全的?
- 这个循环测试条件是什么意思?
- Haskell中用多态性替换条件的等效模式是什么?
- 计算文本文件中行数的最佳条件是什么
- 在条件上更新变量的最快方法是什么?
- 同时执行 if 和 else 条件的逻辑是什么
- 在发出等待条件变量的信号后,线程何时获取锁?是什么决定了它
- 这个奇怪的条件运算符语法是什么
- 编译器减少 std::copy to memcpy (memmove) 的条件是什么?
- 条件是什么意思
- 有条件地控制 for 循环方向的最佳方法是什么
- while 循环的两个条件。我的错误是什么?
- 条件语句在比较表达式时的基础是什么?
- 在 C++11 中等待多个条件变量的最佳方法是什么?
- 动态绑定的条件到底是什么
- if()语句使用条件的目的是什么?C++
- 移动构造函数的后置条件是什么
- 使用条件语句激活函数的目的是什么
- 逗号在条件语句中的优点是什么
- 在选择函数中等待fd的异常条件是什么?