C++ Primer 学习笔记——第八章

发布时间 2023-07-30 21:04:40作者: 木木亚伦

第八章 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关联的文件是否成功打开且尚未关闭

使用文件流对象

当打算读取某个文件时,我们需要定义一个文件流对象,并将其与文件关联起来。每个文件流对象都定义了一个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

当我们的某些工作是对整行文本进行处理,而其他工作是处理行内的单个单词时,通常考虑使用istringstream。

当我们逐步构造输出,希望最后一起打印时,ostringstream通常是我们的一般解。

总结

fstream和sstream都继承于iostream,所以在操作上三者具有很多相同点。

对于声明在语句外的流,可以通过在语句块内部使用clear函数解决EOF问题。

条件状态的特性需要记牢!