91.IO类
为了支持不同种类的IO处理操作,在istream和ostream之外,标准库还定义了其他一些IO类型。 下表列出了这些类型,分别定义在三个独立的头文件中:iostream定义了用于读写流的基本类型,fstream定义了读写 命名文件的类型,sstream定义了读写内存string对象的类型。
表8.1: IO库类型和头文件 | |
头文件 | 类型 |
iostream | istream,wistream从流读取数据 |
ostream, wostream向流写入数据 | |
iostream, wiostream读写流 | |
fstream | ifstream,wifstream从文件读取数据 |
ofstream,wofstream向文件写入数据 | |
fstream,wfstream读写文件 | |
sstream | istringstream,wistringstream从string读取数据 |
ostringstream, wostringstream向string写入数据 | |
stringstream, wstringstream读写string |
为了支持使用宽字符的语言,标准库定义了一组类型和对象来操纵wchar_t类型的数据 (参见2.1.1节, 第30页)。 宽字符版本的类型和函数的名字以一个w开始。例如wcin、 wcout 和wcerr是分别对应cin、 cout 和cerr的宽字符版对象 。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。 例如, 头文件fstream定义了ifstream和wifstream类型。
IO类型间的关系
概念上,设备类型和字符大小都不会影响我们要执行的IO操作。例如,我们可以用>>读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个string读取。类似的,我们也不用管读取的字符能存入一个char对象内,还是需要一个wchar_t对象来存储。
标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制(inheritance) 实现的。利用模板(参见3.3节, 第87页),我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。 我们将在第15章和18.3节(第710页)介绍C++是如何支持继承机制的。
简单地说,继承机制使我们可以声明一个特定的类继承自另一个类。 我们通常可以将派生类(继承类)对象当作其基类(所继承的类)对象来使用。
类型ifstream和istringstream都继承自istream。因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象。也就是说,我们是如何使用cin 的,就可以同样地使用这些类型的对象。例如, 可以对一个ifstream或istringstream对象调用getline,也可以使用>>从一个ifstream或istringstream对象中读取数据。类似的,类型ofstream和ostringstream都继承自ostream。因此,我们是如何使用cout 的,就可以同样地使用这些类型的对象。
1IO对象无拷贝或赋值
不能拷贝或对IO对象赋值:
ofstream out1, out2;
out1 = out2;//错误:不能对流对象赋值
ofstream print(ofstream);//错误:不能初始化ofstream参数
out2 = print(out2);//错误:不能拷贝流对象
由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型(参见6.2.1节,第188页)。 进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变第188页)。 进行 10操作的函数通常以引用方式传递和返回流。 读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的 。
2.条件状态
表8.2: IO库条件状态 | |
stm::iostate | stm是一种IO类型,在表8.1(第278页)中已列出。iostate是一种机器相关的类型,提供了表达条件状态的完整功能 |
stm::badbit | stm::badbit用来指出流已崩溃 |
stm::failbit | stm::failbit用来指出一个IO操作失败了 |
stm::eofbit | stm::eofbit用来指出流到达了文件结束 |
stm::goodbit | stm::goodbit用来指出流未处于错误状态。此值保证为零 |
ostream, wostream向流写入数据 | ostream, wostream向流写入数据 |
s.eof() | 若流s的eofbit置位,则返回true |
s.fail() | 若流s的failbit或badbit置位,则返回true |
s.bad() | 若流s的badbit置位,则返回true |
s.good() | 若流s处于有效状态,则返回true |
s.clear() | 将流s中所有条件状态位复位,将流的状态设置为有效。返回void |
s.clear(flags) | 根据给定的flags标志位,将流s中对应条件状态位复位。flags的类型为strm::iostate。返回void |
s.setstate(flags) | 根据给定的flags标志位,将流s中对应条件状态位四位。flags的类型为strm::iostate。返回void |
s.rdstate() | 返回流s的当前条件状态,返回值类型为strm::iostate |
下面是一个IO错误的例子:
int ival;
cin >> ival;
如果我们在标准输入上键入Boo,读操作就会失败。代码中的输入运算符期待读取一个int,但却得到了一个字符B。这样,cin会进入错误状态。类似的,如果我们输入一个文件结束标识,cin也会进入错误状态。
一个流一旦发生错误,其上后续的IO操作都会失败。只有当一个流处于无错状态时,我们才可以从它读取数据,向它写入数据。由于流可能处于错误状态,因此代码通常应该在使用一个流之前检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用:
while (cin >> word)
// ok:读操作成功......
while循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。
2.1查询流的状态
将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。有时我们也需要知道流为什么失败。例如,在键入文件结束标识后我们的应对措施,可能与遇到一个IO设备错误的处理方式是不同的。
lO库定义了一个与机器无关的iostate类型,它提供了表达流状态的完整功能。这个类型应作为个位集合来使用,IO库定义了4个iostate类型的constexpr值(参见2.4.4节,第58页),表示特定的位模式。这些值用来表示特定类型的IO条件,可以与位运算符(参见4.8节,第137页)一起使用一次性检测或设置多个标志位。
badbit表示系统级错误,如不可恢复的读写错误。通常情况下,一旦badbit被置位,流就无法再使用了。在发生可恢复错误后,failbit被置位,如期望读取数值却读出一个字符等错误。这种问题通常是可以修正的,流还可以继续使用。如果到达文件结束位置,eofbit和failbit都会被置位。goodbit的值为0,表示流未发生错误。如果badbit、failbit和eofbit任一个被置位,则检测流状态的条件会失败。
标准库还定义了一组函数来查询这些标志位的状态。操作good在所有错误位均未置位的情况下返回true,而bad、 fail和eof则在对应错误位被置位时返回true。 此外,在badbit被置位时,fail过也会返回true。这意味着,使用good或fail是确定流的总体状态的正确方法。实际上,我们将流当作条件使用的代码就等价于!fail()。 而eof和bad操作只能表示特定的错误。
2.2管理条件状态
流对象的rdstate成员返回一个iostate值,对应流的当前状态。setstate操作将给定条件位置位,表示发生了对应错误。clear成员是一个重载的成员(参见6.4节,第206页):它有一个不接受参数的版本,而另一个版本接受一个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不变:
//复位failbot和badbit, 保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin. badbit);
3.管理输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。例如,如果执行下面的代码
os << "please enter a value: ";
文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。 由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。
导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:
●程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
●缓冲区满时,需要刷新缓冲,而后新的数据才能继续写入缓冲区。
●我们可以使用操纵符如endl(参见1.2节,第6页)来显式刷新缓冲区。
●在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区。默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的。
●一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联到 的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,
读cin或写cerr都会导致cout的缓冲区被刷新。
3.1刷新输出缓冲区
我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。IO库中还有两个类似的操纵符:flush和ends。flush刷新缓冲区,但不输出任何额外的字符;ends向缓冲区插入一个空字符,然后刷新缓冲区:
cout << "hi!" << endl;//榆出hi和一个换行,然后刷新缓冲区
cout << "hi!" << flush;//输出hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!"<< ends;//输出hi和一个空字符,然后刷新缓冲区
3.2unitbuf操纵符
如果想在每次输出操作后都刷新缓冲区,我们可以使用unibuf操纵符。它告诉流在接下来的每次写操作之后都进行一次flush操作。而nounitbuf操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制:
cout << unitbuf;//所有输出操作后都会立即刷新缓冲区
//输出都立即刷新,无缓冲
cout << nounitbuf;//回到正常的缓冲方式
警告:如果程序崩溃,输出缓冲区不会被刷新
如果程序异常终止,输出缓冲区是不会被刷新的。当一个程序崩溃后,它所输出的数据很可能停留在输出缓冲区中等待打印。
当调试一个已经崩溃的程序时,需要确认那些你认为巳经输出的数据确实已经刷新了。否则,可能将大量时间浪费在追踪代码为什么没有执行上,而实际上代码已经执行了,只是程序崩溃后缓冲区没有被刷新,输出数据被挂起没有打印而已。
3.3关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将cout和cin关联在一起,因此下面语句
cin >> ival;
导致cout的缓冲区被刷新。
注意:
交互式系统通常应该关联输入流和输出流。这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。
tie有两个重载的版本(参见6.4节,第206页):一个版本不带参数,返回指向输出流的指针。如果本对象当前关联到一个输出流,则返回的就是指向这个流的指针,如果对象未关联到流,则返回空指针。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应该关联到cout
cin.tie(&cerr);//读取cin会刷新cerr而不是cout
cin.tie(old_tie);//重建cin和cout间的正常关联
在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了tie。为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流 ,但多个流可以同时关联到同一个ostream。
参考资料:
C++ Primer