就我个人而言,我认为这些都是合理的问题,而且我清楚地记得我自己也曾为这些问题而苦苦挣扎。那么我们开始吧:
我的错误在哪里?
我不会称其为mistake但你可能想确保你不必放弃所读的内容。也就是说,我将实现三个版本的输入函数。根据特定类型解码的复杂程度,我什至可能不会共享代码,因为无论如何它可能只是一小部分。如果超过一两行,可能会共享代码。也就是说,在你的例子中我会有一个提取器FooBar
本质上是读取Foo
or the Bar
成员并相应地初始化对象。或者,我会阅读主要部分,然后调用提取公共数据的共享实现。
让我们来做这个练习,因为有一些事情可能会变得复杂。根据您对格式的描述,我不清楚“字符串”以及字符串后面的内容是否被分隔,例如通过空格(空格、制表符等)。如果没有,你不能只阅读std::string
:它们的默认行为是读取直到下一个空格。有多种方法可以调整流以将字符视为空白(使用std::ctype<char>
)但我只是假设有空间。在这种情况下,提取器Foo
可能看起来像这样(注意,所有代码都是entirely未经测试):
std::istream& read_data(std::istream& is, Foo& foo, std::string& s) {
Foo tmp(s);
if (is >> get_char<'('> >> tmp.m_x >> get_char<','> >> tmp.m_y >> get_char<')'>)
std::swap(tmp, foo);
return is;
}
std::istream& operator>>(std::istream& is, Foo& foo)
{
std::string s;
return read_data(is >> s, foo, s);
}
这个想法是read_data()
阅读 a 的一部分Foo
这不同于Bar
当读一个FooBar
。类似的方法将用于Bar
但我忽略了这一点。更有趣的是这个有趣的用法get_char()
函数模板。这是一种叫做机械手并且只是一个以流引用作为参数并返回流引用的函数。由于我们想要读取和比较不同的字符,因此我将其作为模板,但每个字符也可以有一个函数。我只是懒得打字:
template <char Expect>
std::istream& get_char(std::istream& in) {
char c;
if (in >> c && c != 'e') {
in.set_state(std::ios_base::failbit);
}
return in;
}
我的代码看起来有点奇怪,因为几乎没有检查事情是否有效。那是因为流只会设置std::ios_base::failbit
当阅读会员失败时,我真的不必打扰自己。实际上添加特殊逻辑的唯一情况是get_char()
处理对特定字符的期待。类似地,也不会跳过空白字符(即使用std::ws
) 继续:所有输入功能都在formatted input
函数并且默认情况下会跳过空格(您可以使用例如将其关闭)in >> std::noskipws
)但是很多事情都行不通。
使用类似的实现来读取Bar
,读一个FooBar
看起来像这样:
std::istream& operator>> (std::istream& in, FooBar& foobar) {
std::string s;
if (in >> s) {
switch ((in >> std::ws).peek()) {
case '(': { Foo foo; read_data(in, foo, s); foobar = foo; break; }
case '[': { Bar bar; read_data(in, bar, s); foobar = bar; break; }
default: in.set_state(std::ios_base::failbit);
}
}
return in;
}
这段代码使用了一个未格式化的输入功能,peek()
它只查看下一个字符。它要么返回下一个字符,要么返回std::char_traits<char>::eof()
如果失败了。因此,如果有左括号或左方括号,我们有read_data()
接管。否则我们总是会失败。解决了眼前的问题。继续发布信息...
是否应该将其调用写入操作员>>以使初始数据在故障后仍然可用?
一般的答案是:不。如果你没能读懂,就会出现问题,你就会放弃。不过,这可能意味着您需要更加努力才能避免失败。如果您确实需要从原来的位置退出来解析数据,您可能需要先将数据读入std::string
using std::getline()
然后分析这个字符串。用于std::getline()
假设有一个明显的字符可以停下来。默认值是换行符(因此得名),但您也可以使用其他字符:
std::getline(in, str, '!');
这将在下一个感叹号处停止并将其之前的所有字符存储在str
。它还会提取终止字符,但不会存储它。有时,当您读取可能没有换行符的文件的最后一行时,这会变得很有趣:std::getline()
如果它可以读取至少一个字符,则成功。如果您需要知道文件中的最后一个字符是否是换行符,您可以测试流是否到达:
if (std::getline(in, str) && in.eof()) { std::cout
如果是这样,我怎样才能有效地做到这一点?
流本质上是单次传递:您只接收每个字符一次,如果您跳过一个字符,则会消耗它。因此,您通常希望以不必回溯的方式构建数据。也就是说,这并不总是可能的,大多数流实际上在引擎盖下都有一个缓冲区,可以返回两个字符。由于流可以由用户实现,因此不能保证可以返回字符。即使对于标准流也没有真正的保证。
如果你想返回一个字符,你必须准确地放回你提取的字符:
char c;
if (in >> c && c != 'a')
in.putback(c);
if (in >> c && c != 'b')
in.unget();
后一个函数的性能稍好一些,因为它不必检查字符是否确实是提取的字符。它失败的机会也更少。理论上,您可以放回任意数量的字符,但大多数流在所有情况下都不会支持多个字符:如果有缓冲区,标准库会负责“取消获取”所有字符,直到缓冲区开始到达了。如果返回另一个字符,则调用虚函数std::streambuf::pbackfail()
这可能会或可能不会提供更多可用的缓冲区空间。在我实现的流缓冲区中,它通常会失败,即我通常不会覆盖此函数。
如果没有,是否有办法“存储”(和恢复)输入流的完整状态:状态和数据?
如果你想完全恢复你当时的状态,包括角色,答案是:当然有。 ...但不是easy方式。例如,您可以实现过滤流缓冲区并按上述方式放回字符以恢复要读取的序列(或支持在流中查找或显式设置标记)。对于某些流,您可以使用查找,但并非所有流都支持此功能。例如,std::cin
通常不支持查找。
不过,恢复角色只是故事的一半。您想要恢复的其他内容是状态标志和任何格式化数据。事实上,如果流进入失败甚至糟糕的状态,您需要在流执行大多数操作之前清除状态标志(尽管我认为格式化内容无论如何都可以重置):
std::istream fmt(0); // doesn't have a default constructor: create an invalid stream
fmt.copyfmt(in); // safe the current format settings
// use in
in.copyfmt(fmt); // restore the original format settings
功能copyfmt()
复制与流关联且与格式相关的所有字段。这些都是:
- 语言环境
- fmt 标志
- 信息存储 iword() 和 pword()
- 流的事件
- 例外情况
- 流的状态
如果您不了解其中的大多数内容,请不要担心:大多数内容您可能不会关心。好吧,直到您需要它为止,但到那时您已经希望获得一些文档并阅读它(或询问并得到良好的答复)。
failbit 和 badbit 之间有什么区别?我们什么时候应该使用其中之一?
最后是一个简短而简单的:
-
failbit
当检测到格式错误时设置,例如应为数字,但找到字符“T”。
-
badbit
当流的基础设施出现问题时设置。例如,当未设置流缓冲区时(如在流中fmt
上面)流有std::badbit
放。另一个原因是如果抛出异常(并通过exceptions()
面具;默认情况下,所有异常都会被捕获)。
是否有任何在线参考资料(或一本书)深入解释如何处理 iostreams ?不仅仅是基本的东西:完整的错误处理。
啊,是的,很高兴你问。您可能想要获取 Nicolai Josuttis 的“C++ 标准库”。我知道这本书描述了所有细节,因为我参与了这本书的写作。如果你真的想知道一切关于 IOStreams 和区域设置,您需要 Angelika Langer 和 Klaus Kreft 的“IOStreams 和区域设置”。如果您想知道我最初从哪里获得信息:这是 Steve Teale 的“IOStreams”,我不知道这本书是否仍在印刷,并且它缺少标准化过程中引入的很多内容。由于我实现了自己的 IOStreams 版本(和语言环境),所以我也了解这些扩展。