到目前为止,我们已经使用过的IO类型和对象都是操纵 char 数据的。默认情况下,这些对象都是关联到用户的控制台窗口的。当然,我们不能限制实际应用程序仅从控制台窗口进行 IO 操作,应用程序常常需要读写命名文件。而且,使用IO 操作处理 string 中的字符会很方便。
此外,应用程序还可能读写需要宽字符支持的语言。
为了支持这些不同种类的IO 处理操作,在istream和ostream之外,标准库还定义了其他一些 IO 类型,我们之前都已经使用过了。表 8.1 列出了这些类型,分别定义在三个独立的头文件中∶ iostream 定义了用于读写流的基本类型,fstream定义了读写命名文件的类型,sstream定义了读写内存 string 对象的类型。
IO对象无拷贝或赋值
ofstream out1, out2;
out1 = out2; // 错误:不能对流对象赋值
ofstream print(ofstream); // 错误:不能初始化ofstream参数
out2 = print(out2); // 错误:不能拷贝流对象
由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。
进行 IO操作的函数通常以引用方式传递和返回流。读写一个IO 对象会改变其状态,因此传递和返回的引用不能是 const 的。
条件状态
IO 操作一个与生俱来的问题是可能发生错误。一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。表 8.2 列出了 IO 类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态(condition state)。
下面是一个错误的IO例子
int ival;
cin >> ival;
如果我们在标准输入上键入 Boo,读操作就会失败。代码中的输入运算符期待读取一个 int,但却得到了一个字符 B。这样,cin 会进入错误状态。
类似的,如果我们输入一个文件结束标识,cin 也会进入错误状态。
一个流一旦发生错误,其上后续的IO 操作都会失败。只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用∶
while (cin >> word)
// 读操作成功
while 循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。
查询流的状态
将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。
有时我们也需要知道流为什么失败。例如,在键入文件结束标识后我们的应对措施,可能与遇到一个 IO 设备错误的处理方式是不同的。
IO 库定义了一个与机器无关的iostate 类型,它提供了表达流状态的完整功能。这个类型应作为一个位集合来使用,IO库定义了4个iostate类型的constexpr值,表示特定的位模式。
这些值用来表示特定类型的IO条件,可以与位运算符一起使用来一次性检测或设置多个标志位。
badbit 表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit 被置位,流就无法再使用了。
在发生可恢复错误后,failbit 被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。
如果到达文件结束位置,eofbit和failbit都会被置位。
goodbit的值为0,表示流未发生错误。如果 badbit、failbit 和 eofbit 任一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态。
操作 good在所有错误位均未置位的情况下返回 true,而 bad、fail和eof 则在对应错误位被置位时返回true。
此外,在badbit被置位时,fail也会返回true。
这意味着,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。而 eof 和 bad 操作只能表示特定的错误。
管理条件状态
流对象的rdstate 成员返回一个iostate 值,对应流的当前状态。
setstate操作将给定条件位置位,表示发生了对应错误。
clear 成员是一个重载的成员:它有一个不接受参数的版本,而另一个版本接受一个iostate类型的参数。
clear不接受参数的版本清除(复位)所有错误标志位。执行clear()后,调用good会返回 true。我们可以这样使用这些成员∶
// 己住cin的当前状态
auto old_state = cin.rdstate(); // 记住cin的当前状态
cin.clear(); // 使cin有效
process_input(cin); // 使用cin
cin.setstate(old_state); // 将cin置为原有状态
带参数的 clear 版本接受一个iostate 值,表示流的新状态。为了复位单一的条件状态位,我们首先用 rdstate 读出当前条件状态,然后用位操作将所需位复位来生成新的状态。
例如,下面的代码将failbit 和 badbit复位,但保持eofbit 不变∶
// 复位failbit和badbit,其他位置保持不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
例子:
#include <iostream>
#include <stdexcept>
using namespace std;
istream & f(istream & in){
int v;
while (in >> v, !in.eof()){
if (in.bad())
throw runtime_error("IO流错误");
if (in.fail()){
cerr << "数据错误,请重试:" << endl;
in.clear();
in.ignore(100, '
');
continue;
}
cout << v << endl;
}
in.clear();
return in;
}
int main(){
cout << "" << endl;
f(cin);
return 0;
}
输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码
os << "Please enter a value.";
文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。
导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多∶
- 程序正常结束,作为 main 函数的 return操作的一部分,缓冲刷新被执行。
- 缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
- 我们可以使用操纵符如endl来显式刷新缓冲区。
- 在每个输出操作之后,我们可以用操纵符 unitbuf 设置流的内部状态,来清空缓冲区。默认情况下,对 cerr 是设置 unitbuf 的,因此写到cerr的内容都是立即刷新的。
- 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin 和cerr 都关联到cout。因此,读 cin 或写 cerr 都会导致 cout 的缓冲区被刷新。
刷新输出缓冲区
我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。IO 库中还有两个类似的操纵符:flush 和ends。
flush 刷新缓冲区,但不输出任何额外的字符; ends向缓冲区插入一个空字符,然后刷新缓冲区:
cout << "Hi!" << endl; // 输出hi 和一个换行,然后刷新缓冲区
cout << "Hi!" << flush; // 输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "Hi!" << ends; // 输出hi和一个空字符,然后刷新缓冲区
unit操纵符
如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操纵符。
它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制∶
cout << unitbuf; // 所有输出操作后会立即刷新缓冲区
cout << nounitbuf; // 回到正常模式
关联输入输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将 cout 和 cin 关联在一起,因此下面语句
cin >> ival;
导致cout的缓冲区被刷新。
tie有两个重载版本:一个版本带参数,返回指向输出流的指针。
如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。
tie 的第二个版本接受一个指向 ostream的指针,将自己关联到此 ostream。
即,x.tie(&o)将流x关联到输出流o。
我们既可以将一个istream 对象关联到另一个ostream,也可以将一个ostream关联到另一个 ostream∶
cin.tie(&cout); // 仅仅用来展示:标准库将cin和cout关联在一起
// old_tie指向当前关联到cin的流
ostream *old_tie = cin.tie(nullptr); // cin不再与其他流关联
// 将cin与cerr关联
cin.tie(&cerr); // cin会刷新cerr而不是cout
cin.tie(old_tie); // 重建cin与cout的正常关系
在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了 tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个 ostream。