C++ Primer 学习笔记——第八章
第八章 IO库
前言
C++语言并不会直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备中读取数据、向设备写入数据IO操作。设备可以是文件、控制台窗口等,还有一些类型允许访问内存IO。
IO库定义了读写内置类型值的操作。
8.1 IO类
在之前我们使用的IO类型和对象都是操作char数据且这些对象都是关联到用户的控制台窗口。但在实际开发中还不够,所以在C++的IO操作中还包括以下类型供开发者使用。
在头文件iostream定义了读写流的基本类型、fstream定义了读写命名文件的类型、sstream定义了读写内存string对象的类型。
头文件 | 类型 |
---|---|
iostream | istream,wistream 从流读取数据 ostream,wostream向流写入数据 iostream,wiostream读写流 |
fstream | ifstream,wifstream从文件读取数据 ofstream,wofstream向文件写入数据 fstream,wfstream读写文件 |
sstream | istringstream,wistringstream从stream读取数据 ostringstream,wostringstream向string写入数据 stringstream,wstringstream读写string |
为了支持宽字符的语言,标准库定义了一组类型与对象来操作wchar_t类型的数据。宽字符版本的类型和函数的名字以一个w开始。
从概念上讲,IO操作并不会因为设备类型和字符大小而受到影响。例如,我希望通过某个文件读取宽字符数据,其与在终端窗口读取普通字符数据其操作都是一致的,都是通过输入运算符>>。那么这样就存在一个好处,我们可以忽略不同类型的流之间的差异(但并不是不存在差异),使得开发效率得到提高。
这种忽略流差异的技术通过继承机制(inheritance)实现,利用模板,通过使用具有继承关系的类使得我们忽略工作细节。
IO对象无拷贝或赋值
如标题,IO对象不存在拷贝或者赋值初始化的操作:
ofstream of_1,of_2;
of_1=of_2; /* 错误:无法对流对象进行赋值 */
ofstream print(ofstream); /* 错误:无法初始化ofstream参数 */
of_2=print(of_2); /* 错误:无法拷贝流对象 */
由此引申出,无法将返回类型或者形参设置为流类型,同时由于读写一个IO对象会改变其状态,所以常常使用引用方式传递和返回流且此引用不能为const。
状态条件
IO操作并不是万无一失的,其潜在可能发生的错误,有一些错误能够较为容易修复,但是有一些错误其可能在系统层面,其修复的范围远远超过应用层面,这时就需要一些IO操作上的函数或者标志来帮助程序确定IO操作状态,其称为访问和操作流的
条件状态(condition state)。
状态名 | 解释 |
---|---|
strm::iostate | iostate是一种机器相关的类型,提供了表达条件状态的完整功能 |
strm::badbit | 指出流已经崩溃 |
strm::failbit | 指出一个IO操作失败 |
strm::eofbit | 指出流已经到达文件结束 |
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流中对应的条件状态位复位。 flags的类型为strm.iostate,返回void |
s.setstate(flags) | 根据给定的flags标志位,将s流中对应的条件状态位置位。 flags的类型为strm.iostate,返回void |
s.rdstate() | 返回s流的当前条件状态。 返回值的类型为:strm::iostate |
strm是一种IO类型,s为流
当一个流发生错误,那么后续的IO操作都会失败,为了程序的健壮性,通常需要使用流之前判断其是否处于良好状态。最简单的方式:
while(cin>>word)
/* ok,next */
当流出现问题,我们肯定希望查询到错误原因,这个时候就需要依赖条件状态,IO库定义了一个与机器相关的iostate类型,其提供表达流状态的完整功能,作为一个位集合使用。通过位运算符进行一次性检测或者设置多个标志位。
具体来讲:
- badbit,系统级错误,例如:不可恢复的读写错误。当badbit被置位,流就无法再使用
- failbit,发生可恢复错误,例如:期望读取数值结果读取字符,当错误被修复,流还可以使用。
- 当读取文件结束,eofbit和failbit都会被置位,goodbit值为0,表示流未发生错误。
- 如果badbit、failbit和eofbit任意一个被置位,则检测状态的条件会失败。
在上述描写到,IO库定义的一系列查询标志位的函数,当错误位被置位时其对应的函数就会返回true,注意一点,无论是badbit还是eofbit还是其本身failbit被置位,都会同时触发fail()
函数,所以在上述判断流状态的条件代码实际等价于!fail()
。
管理条件状态
在上述列表上介绍了四种管理条件状态的函数,我们可以使用clear()
清除所有错误标志位,也可以使用clear(flags)
清除指定的错误标志位,例如:
/* 假设cin出现所有的错位状态位 */
/* 希望复位单一状态位 */
cin.clear(cin.rdstate()&~cin.failbit&~cin.badbit);
我们可以通过读取当前状态,例如上述代码,我们就可以复位failbit和badbit,但是eofbit保持不变。
管理输出缓冲
每个输出流都管理一个缓冲区,用来保存程序读写的数据。
缓冲机制的存在可以带来很大的性能提升,操作系统可以将(多个)程序的(多个)输出操作组合成为单一的系统级别写操作。
例如:
cout<<"hello";
cout<<"world";
cout<<endl;
cout<<"!";
在前两行表达式就是将输出操作组合在一起,都存放在缓冲区。第三行则进行缓冲区的刷新,那么第四行表达式中数据就和前两行数据不存放在一起(前两行数据已经被刷新掉了)。
导致缓冲刷新的原因有很多,例如:
- 程序正常结束,作为main函数的return操作的一部分,缓冲刷新将会被执行
- 缓冲区已满,后入的数据只有在刷新缓冲区后才能继续写入缓冲区
- 使用操作符endl显式刷新缓冲区
- 在每个输出操作之后,使用操作符unitbuf设置流的内部状态,借此清空缓冲区
- 默认情况下,对cerr是设置unitbuf的,因此写到cerr的内容都是立即刷新的
- 一个输出流可能被关联到另一个流
- 当读写被关联的流时,关联到的流的缓冲区会被刷新。例如,默认情况下,cin和cerr都关联到cout。因此,读cin或者写cerr都会导致cout的缓冲区被刷新。
刷新输出缓冲区
在此之前,我们使用操作符endl来进行换行和刷新缓冲区(当时我们可能还没注意到endl具有刷新缓冲区的功能)。类似的,IO库中还存在flush和ends两种操作符也可以执行刷新操作。
flush刷新缓冲区,但是不输出任何额外的字符(类似endl但不换行);ends向缓冲区插入一个空字符,然后刷新缓冲区。
如果想要在每次输出操作后都执行刷新缓冲区的操作,那么我们可以使用unitbuf操作符。它告诉流在接下来的每次写操作之后都会进行一次flush操作。nounitbuf操作符则是重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。
cout<<unitbuf; /* 下面所有的输出操作均会立即刷新缓冲区 */
/* .... */
cout<<nounitbuf; /* 回到正常的缓冲方式 */
注意
如果程序崩溃,输出缓冲区是不会被刷新的
如果程序异常终止,其缓冲区不会被刷新。其所输出的数据很可能停留在输出缓冲区中等待打印
注意这个细节,如果我们调试一个已经崩溃的程序,需要检查认为已经输出的数据确实已经被刷新了,否则追踪一个没有价值的代码是毫无意义的。
关联输入和输出流
当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。
在IO库中,cin默认已经和cout关联在一起,也就是说当执行cin语句时,在此之前的cout的缓冲区
将会被刷新。
开发
交互式系统通常应该关联输入流和输出流。这意味着所有的输出,包括用户提示信息,都会在读操作之前被打印出来。
我们可以通过tie函数关联流。
cin.tie(&cout); /* 将cin和cout关联 */
cin.tie(nullptr); /* cin不再和其他流关联 */
cin.tie(&cerr); /* cin与cerr关联 */
8.2 文件输入输出
在IO库中,头文件fstream定义了三个类型来支持文件IO操作。
- ifstream,从给定文件读取数据
- ofstream,向给定文件写入数据
- fstream,向给定文件读写数据
ifstream继承于iostream,所以常规操作与cin和cout对象操作类似,同样可以使用IO运算符(<<和>>)来读写文件,也可以使用getline从一个ifstream读取数据。
除此之外,fstream还具有一些自己独特的操作:
操作 | 解释 |
---|---|
fstream fstrm; | 创建一个未绑定的文件流 |
fstream fstrm(s); | 创建一个fstream,并打开名为s的文件。s可以是string类型,也可以是指向C风格字符串的指针。同时其构造函数都是explicit的。默认的文件模式mode依赖于fstream的类型 |
fstream fstrm(s,mode); | 与上述类似,但是按照指定mode打开文件 |
fstrm.open(s) | 打开名为s的文件,并将文件与fstrm绑定。s的类型与上述类似,默认的文件mode依赖于fstream的类型。返回void |
fstrm.close() | 关闭与fstrm绑定的文件。返回void |
fstrm.is_open() | 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭 |
fstream是头文件fstream中定义的一个类型,可以是ifstream,也可以是ofstream,当然也可以是fstream。
使用文件流对象
当打算读取某个文件时,我们需要定义一个文件流对象,并将其与文件关联起来。每个文件流对象都定义了一个open成员函数,其完成一些系统相关的操作(定位给定文件,视情况打开读或写模式)
创建文件流对象,我们可以选择提供文件名(当然,也可以后续提供)。如果提供一个文件名,那么open将会自动调用。
ifstream input(ifile); /* 构造一个ifstream并打开给定文件 */
ofstream output; /* 构造一个ofstream对象,并未关联文件 */
文件名可以是string对象,也可以是C风格字符数组。在C++11版本之前仅允许C风格字符数组。
用fstream代替iostream&
在前文提到,在要求使用基类型对象的地方,我们可以用继承类型的对象来代替。也就是说,在一个接受iostream类型引用(指针)参数的函数,可以使用一个对应的fstream(或者sstream)类型来调用。
成员函数open和close
在前文提到,我们可以定义一个文件流对象,但不将其与文件关联起来。
关联文件使用文件流对象的成员函数open,
ifstream input(ifile); /* 构造一个ifstream并给定文件 */
ofstream output; /* 构造一个ofstream对象,但不关联文件 */
output.open(ifile); /* 关联指定文件 */
但一个文件流已经被打开了,那么其就保持与对应文件的关联。这个时候,希望再次调用成员函数open将会失败且failbit会被置位。如果希望文件流关联另一个文件,需要关闭已经关联的文件。这个时候就需要用到成员函数close。
input.close(); /* 关闭文件 */
input.open(ifile_1); /* 关联另一个文件 */
如果open成功,那么open将会设置流的状态,这个时候IO库条件状态good()将会为true。
当一个fstream对象离开其作用域,与之关联的文件会自动关闭。当一个fstream对象被销毁时,close会自动调用。
开发
由于调用open可能会失败,所以在进行open时应该习惯于进行是否成功的检测。
文件模式
每个文件流都有一个关联的文件模式(file mode),用来指出如何使用文件
文件模式 | 解释 |
---|---|
in | 以读方式打开 |
out | 以写方式打开 |
app | 每次写操作前均定位到文件末尾 |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制方式进行IO |
每个文件流类型都定义了一个默认的文件模式。ifstream默认以in模式打开,ofstream默认以out模式打开,fstream默认以in和out模式打开。
无论那种方式打开文件,我们都可以指定文件模式。当然指定是有限制的:
- 只可以对ofstream或者fstream对象设定out模式
- 只可以对ifstream或者fstream对象设定in模式
- trunc模式的设定前提是out模式被设定
- app模式设定前提是trunc模式没有被设定。在app模式下,即便没有显式指定out模式,文件也总是以输出方式被打开
- 默认情况下,out模式打开的文件是会被截断的(即便没有设定trunc),如果想要保留则需要指定app模式,或者同时指定in模式,使打开文件同时进行读写操作
- ate和binary模式可用用于任何类型的文件流对象,且可以与其他任何文件模式组合使用
在上文我们提到out模式默认会截断,即清空文件已有数据。所以如果希望保留则需要显式指定app或者in模式。
8.3 string流
sstream头文件定义了三种类型来支持内存IO。
和fstream和iostream类似,sstream的三种类型分别对string读取数据、写入数据和读取写入数据:istringstream、ostringstream、stringstream。
当然,除了继承iostream的操作,sstream也具有对内存IO类型的特殊操作:
操作 | 解释 |
---|---|
sstream strm; | strm是一个未绑定的sstream对象 |
sstream strm(s); | strm作为sstream对象,保存string s的一个拷贝 |
strm.str() | 返回strm所保存的string的拷贝 |
strm.str(s) | 将string s拷贝到strm中。,返回void |
sstream是头文件sstream中定义的一个类型。
当我们的某些工作是对整行文本进行处理,而其他工作是处理行内的单个单词时,通常考虑使用istringstream。
当我们逐步构造输出,希望最后一起打印时,ostringstream通常是我们的一般解。
总结
fstream和sstream都继承于iostream,所以在操作上三者具有很多相同点。
对于声明在语句外的流,可以通过在语句块内部使用clear函数解决EOF问题。
条件状态的特性需要记牢!