C++IO类,输入输出缓冲区,流状态

时间:2024-04-13 11:02:51

我们的程序已经使用了很多IO库设施:

  • istream(输入流)类型,提供输入操作。
  • ostream(输出流)类型,提供输出操作。
  • cin,一个istream对象,从标准输入读取数据。写入到标准错误。
  • cout,一个ostream对象,向标准输出写入数据。
  • cerr,一个ostream对象,通常用于输出程序错误消息,
  • >>运算符,用来从一个istream对象读取输入数据。
  • <<运算符,用来向一个ostream对象写入输出数据。
  • getline函数,从一个给定的istream读取一行数据,存入一个给定的string对象中。

 IO类

到目前为止,我们已经使用过的IO类型和对象都是操纵char数据的。

默认情况下,这些对象都是关联到用户的控制台窗口的。

当然,我们不能限制实际应用程序仅从控制台窗口进行IO操作,应用程序常常需要读写命名文件。而且,使用IO操作处理string中的字符会很方便。此外,应用程序还可能读写需要宽字符支持的语言。

为了支持这些不同种类的IO处理操作,在istream和ostream之外,标准库还定义了其他一些IO类型,我们之前都已经使用过了。

下表列出了这些类型,分别定义在三个独立的头文件中:

  • iostream定义了用于读写流的基本类型,
  • fstream定义了读写命名文件的类型,
  • sstream定义了读写内存string对象的类型。
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类型的数据。宽字符版本的类型和函数的名字以一个w开始。

例如,wcin、 woout 和wcerr是分别对应cin、cout 和cerr的宽字符版对象。宽字符版本的类型和对象与其对应的普通char版本的类型定义在同一个头文件中。

例如,头文件fstream定义了ifstream和wifstream类型。

IO类型间的关系

概念上,设备类型和字符大小都不会影响我们要执行的操作。

例如,我们可以用>>读取数据,而不用管是从一个控制台窗口,一个磁盘文件,还是一个string读取。类似的,我们也不用管读取的字符能存入一个char对象内,还是需要一个wchar_t对象来存储。

标准库使我们能忽略这些不同类型的流之间的差异,这是通过继承机制实现的。

利用模板,我们可以使用具有继承关系的类,而不必了解继承机制如何工作的细节。

类型ifstream和istringstream都继承自istream。

因此,我们可以像使用istream对象一样来使用ifstream和istringstream对象。

也就是说,我们是如何使用cin的,就可以同样地使用这些类型的对象。

例如,可以对一个 ifstream或istringstream对象调用 getline,也可以使用>>从一个ifstream或istringstream对象中读取数据。

类似的,类型ofstream和ostringstream都继承自ostream因此,我们是如何使用cout的,就可以同样地使用这些类型的对象。

本节剩下部分所介绍的标准库流特性都可以无差别地应用于普通流、文件流和string流,以及char或宽字符流版本。

 IO对象无拷贝或赋值

我们不能拷贝或对IO对象赋值:

ofstream outl, out2;
outl = out2; // 错误:不能对流对象赋值
ofstream print (ofstream) ;// 错误:不能初始化ofstream参数
out2 = print (out2); //错误:不能拷贝流对象

由于不能拷贝IO对象,因此我们也不能将形参或返回类型设置为流类型。

进行IO操作的函数通常以引用方式传递和返回流。

读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。

条件状态 

IO 操作一个与生俱来的问题是可能发生错误。

一些错误是可恢复的,而其他错误则发生在系统深处,已经超出了应用程序可以修正的范围。

表中列出了IO类所定义的一些函数和标志,可以帮助我们访问和操纵流的条件状态。

IO库条件状态
strm::iostate strm 是一种IO类型。iostate是一种机器相关的类型,提供了表达条件状态的完整功能
strm::badbit strm::badbit用来指出流已崩溃
strm::failbit strm::failbit 用来指出一个IO操作失败了
stm::eofbit strm::eofbit用来指出流到达了文件结束
stm::goodbit strm::goodbit用来指出流未处于错误状态。此值保证为零
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中对应条件状态位复位。flaqs的类型为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 循环检查>>表达式返回的流的状态。如果输入操作成功,流保持有效状态,则条件为真。

查询流的状态

将流作为条件使用,只能告诉我们流是否有效,而无法告诉我们具体发生了什么。

有时我们也需要知道流为什么失败。

例如,在键入文件结束标识后我们的应对措施,可能与遇到一个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);


 

管理输出缓冲

每个输出流都管理一个缓冲区,用来保存程序读写的数据。

例如,如果执行下面的代码

os << "please enter a value:";

文本串可能立即打印出来,但也有可能被操作系统保存在缓冲区中,随后再打印。

有了缓冲机制,操作系统就可以将程序的多个输出操作组合成单一的系统级写操作。

由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。

导致缓冲刷新(即,数据真正写到输出设备或文件)的原因有很多:

  • 程序正常结束,作为main函数的return操作的一部分,缓冲刷新被执行。
  • 缓冲区满时,需要刷新缓冲,而后续的数据才能继续写入缓冲区。
  • 我们可以使用操纵符,如endl来显式刷新缓冲区。
  • 在每个输出操作之后,我们可以用操纵符unitbuf设置流的内部状态,来清空缓冲区默认情况下,对cerr是设置unitbuf的,因此,写到cerr的内容都是立即刷新的
  • 一个输出流可能被关联到另一个流。在这种情况下,当读写被关联的流时,关联的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都被关联到cout,读cin或写cexr都会导致cout的缓冲区被刷新。

刷新输出缓冲区 

我们已经使用过操纵符endl,它完成换行并刷新缓冲区的工作。

IO库中还有两个类似的操纵符:flush和ends。

  • flush刷新缓冲区,但不输出任何额外的字符;
  • ends向缓冲区插入一个空字符,然后刷新缓冲区:
cout << "hi!" << endl; //输出hi和一个换行,然后刷新缓冲区
cout << "hi!" <<flush;//输出hi,然后刷新缓冲区,不附加任何额外字符

cout << "hi!" << ends;// 输出hi和一个空字符,然后刷新缓冲区

unitbuf 操纵符

如果想在每次输出操作后都刷新缓冲区,我们可以使用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应该关联到cout
cin.tie(&cerr); // 读取cin会刷新cerr而不是cout

cin.tie(old tie); //重建cin和cout间的正常关联


在这段代码中,为了将一个给定的流关联到一个新的输出流,我们将新流的指针传递给了tie。

为了彻底解开流的关联,我们传递了一个空指针。每个流同时最多关联到一个流,但多个流可以同时关联到同一个ostream。