zl程序教程

您现在的位置是:首页 >  后端

当前栏目

2021-10-22 《C++ Primer》学习记录:第8章

C++学习 10 记录 2021 22 primer
2023-09-27 14:27:14 时间

第 8 章 IO 库

8.1 IO 类

I O 库 类 型 和 头 文 件 头文件 类型 iostream istream, wistream 从流读取数据 ostream, wostream 向流写入数据 iostream, wiostream 读写流 fstream ifstream, wifstream 从文件读取数据 ofstream, wofstream 向文件写入数据 fstream, wfstream 读写文件 sstream istringstream, wistringstream 从 string 读取数据 ostringstream, wostringstream 向 string 写入数据 stringstream, wstringstream 读写 string \begin{array}{c} \hline \bold{IO 库类型和头文件}\\ \hline \begin{array}{l | l} \text{头文件} & \text{类型}\\ \hline \text{iostream} & \text{istream, wistream 从流读取数据}\\ \text{} & \text{ostream, wostream 向流写入数据}\\ \text{} & \text{iostream, wiostream 读写流}\\ \text{fstream} & \text{ifstream, wifstream 从文件读取数据}\\ \text{} & \text{ofstream, wofstream 向文件写入数据}\\ \text{} & \text{fstream, wfstream 读写文件}\\ \text{sstream} & \text{istringstream, wistringstream 从 string 读取数据}\\ \text{} & \text{ostringstream, wostringstream 向 string 写入数据}\\ \text{} & \text{stringstream, wstringstream 读写 string}\\ \end{array}\\ \hline \end{array} IO头文件iostreamfstreamsstream类型istream, wistream 从流读取数据ostream, wostream 向流写入数据iostream, wiostream 读写流ifstream, wifstream 从文件读取数据ofstream, wofstream 向文件写入数据fstream, wfstream 读写文件istringstream, wistringstream  string 读取数据ostringstream, wostringstream  string 写入数据stringstream, wstringstream 读写 string

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

8.1.1 IO 对象无拷贝或赋值
  • 不能拷贝 IO 对象
  • 不能将形参或返回类型设置为流类型,通常函数以引用方式传递和返回流
  • 读写 IO 对象会改变其状态,因此传递和返回的引用不应是 const
ofstream out1, out2;
out1 = out2;                  // 错误:不能对流对象赋值
ofstream print(ofstream);     // 错误:不能初始化 ofstream 参数
out2 = print(out2);           // 错误:不能拷贝流对象
8.1.2 条件状态

I O 库 条 件 状 态 s t r m : : iostate s t r m  表示一种 IO 类型,例如 ofstream。 iostate 是一种机器相关的类型,提供了表达条件状态的完整功能 s t r m : : badbit s t r m : : badbit 用来指出流已崩溃 s t r m : : failbit s t r m : : failbit 用来指出一个 IO 操作失败了 s t r m : : eofbit s t r m : : eofbit 用来指出流达到了文件结束 s t r m : : goodbit s t r m : : goodbit 永安里指出流未处于错误状态。此值保证为 0 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 类型为  s t r m : : iostate。返回 void s.setstate(flags) 根据给定的 flags 标志位,将流 s 中对应条件状态位置位。 flags 类型为  s t r m : : iostate。返回 void s.rdstate() 返回流 s 当前条件状态,返回值类型为  s t r m : : iostate \begin{array}{c} \hline \bold{IO 库条件状态}\\ \hline \begin{array}{l l} strm::\text{iostate} & strm\ \text{表示一种 IO 类型,例如 ofstream。}\\ & \text{iostate 是一种机器相关的类型,提供了表达条件状态的完整功能}\\ strm::\text{badbit} & strm::\text{badbit 用来指出流已崩溃}\\ strm::\text{failbit} & strm::\text{failbit 用来指出一个 IO 操作失败了}\\ strm::\text{eofbit} & strm::\text{eofbit 用来指出流达到了文件结束}\\ strm::\text{goodbit} & strm::\text{goodbit 永安里指出流未处于错误状态。此值保证为 0}\\ \text{s.eof()} & \text{若流 s 的 eofbit 置位,则返回 true}\\ \text{s.fail()} & \text{若流 s 的 failbit 或 badbit 置位,则返回 true}\\ \text{s.bad()} & \text{若流 s 的 badbit 置位,则返回 true}\\ \text{s.good()} & \text{若流 s 处于有效状态,则返回 true}\\ \text{s.clear()} & \text{将流 s 中所有条件状态复位,将流的状态设置为有效,返回 void}\\ \text{s.clear(flags)} & \text{根据给定的 flags 标志位,将流 s 中对应条件状态位复位。}\\ & \text{flags 类型为}\ strm::\text{iostate。返回 void}\\ \text{s.setstate(flags)} & \text{根据给定的 flags 标志位,将流 s 中对应条件状态位置位。}\\ & \text{flags 类型为}\ strm::\text{iostate。返回 void}\\ \text{s.rdstate()} & \text{返回流 s 当前条件状态,返回值类型为}\ strm::\text{iostate}\\ \end{array}\\ \hline \end{array} IOstrm::iostatestrm::badbitstrm::failbitstrm::eofbitstrm::goodbits.eof()s.fail()s.bad()s.good()s.clear()s.clear(flags)s.setstate(flags)s.rdstate()strm 表示一种 IO 类型,例如 ofstreamiostate 是一种机器相关的类型,提供了表达条件状态的完整功能strm::badbit 用来指出流已崩溃strm::failbit 用来指出一个 IO 操作失败了strm::eofbit 用来指出流达到了文件结束strm::goodbit 永安里指出流未处于错误状态。此值保证为 0若流 s  eofbit 置位,则返回 true若流 s  failbit  badbit 置位,则返回 true若流 s  badbit 置位,则返回 true若流 s 处于有效状态,则返回 true将流 s 中所有条件状态复位,将流的状态设置为有效,返回 void根据给定的 flags 标志位,将流 s 中对应条件状态位复位。flags 类型为 strm::iostate。返回 void根据给定的 flags 标志位,将流 s 中对应条件状态位置位。flags 类型为 strm::iostate。返回 void返回流 s 当前条件状态,返回值类型为 strm::iostate

​ 对于下列程序:

int ival;
cin >> ival;

​ 如果我们在标准输入上键入 Boo,读操作就会失败。一个流一旦发生错误,后续的 IO 操作都会失败。只有当其处于无错状态时,才可以从它读取数据。

​ 确定一个流对象的状态,最简单的方法是将其当做一个条件来使用:

while (cin >> word) {
    // ok:操作成功
}

(1)查询流的状态

​ 流作为条件使用,只能告诉我们流是否有效,而无法告知具体信息。

​ IO 库定义了一个与机器无关的 iostate 类型,作为一个位集合来使用。IO 库定义了 4 个 iostate 类型的 constexpr 值,表示特定的位模式。可以用位运算符一起使用来一次性检测或设置多个标志位。

流状态说明
badbit表示系统级错误。一旦被置位,流便无法再使用了。
failbit可恢复错误,这种问题通常可以修正。
eofbit表示文件结束。这时 failbit 也会被置位。
goodbit值为 0 时,表示流未发生错误。
查询函数说明
strm.good()所有错误均未置位,则返回 true。
strm.bad()badbit 被置位,返回 true
strm.fail()failbit 或 badbit 被置位,返回 true
strm.eof()eofbit 被置位,返回 true
  • 如果 badbit、failbit 和 eofbit 任意一个被置位,检测流状态的条件都会失败
  • 使用 good() 或 fail() 是确定流的总体状态的正确方法
  • 事实上,将流当做条件使用的代码等价于 !fail()

(2)管理条件状态

auto old_state = cin.rdstate();  // 记住 cin 的当前状态
cin.clear();                     // 使 cin 有效
process_input(cin);              // 使用 cin
cin.setstate(old_state);         // 将 cin 置为原有状态

​ 我们也可以用位操作将所需位复位来生成新的状态:

// 复位 failbit 和 badbit,保持其他标志位不变
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
8.1.3 管理输出缓冲

​ 每个输出流都管理一个缓冲区,用来保存程序读写的数据。由于设备的写操作可能很耗时,允许操作系统将多个输出操作组合为单一的设备写操作,这样可以带来很大的性能提升。

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

  • 程序正常结束。缓冲刷新作为 main 函数 return 操作的一部分
  • 缓冲区满了
  • 使用操作符手动刷新,如 endl、flush、ends
  • 使用操作符 unitbuf 设置流的内部状态,清空缓冲区。
    • 默认情况下,对 cerr 设置了 unitbuf,因此写到 cerr 的内容都是立即刷新的
  • 一个输出流被关联到另一个流。当另一个流进行读写操作时,该输出流的缓冲区会被刷新。
    • 默认情况下,cin 与 cout 关联,cerr 也与 cout 关联。

(1)刷新输出缓冲区

​ 三个可以刷新缓冲区的操作符:

cout << "hi" << endl;    // 输出 hi 和一个换行符,然后刷新缓冲区
cout << "hi" << flush;   // 输出 hi,然后刷新缓冲区,不附加任何字符
cout << "hi" << ends;    // 输出 hi 和一个空字符,然后刷新缓冲区

(2)unitbuf 操作符

​ 使用 unitbuf 可以在接下来的每次写操作后都进行一次 flush 操作,使用 nounitbuf 重置为原来的状态:

cout << unitbuf;      // 所有输出操作后都会立即刷新缓冲区
...
// 任何输出都立即刷新,无缓冲
...
cout << nounitbuf;    // 回到正常的缓冲方式

(3)关联输入和输出流

​ 当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出流。标准库将 cout 和 cin 关联在一起,因此下面语句

cin >> ival;

​ 导致 cout 的缓冲区被刷新。

交互式系统通常应该关联输入流和输出流。
这意味着所有输出,包括用户提示信息,都会在读操作之前被打印出来。

​ 使用 tie() 函数改变流的关联:

  • tie()

    如果本对象当前关联到一个输出流,返回指向这个输出流的指针;如果对象未关联到流,返回 nullptr。

  • tie(ostream* os)

    将对象关联到输出流 os。

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 之间的正常关联

8.2 文件输入输出

f s t r e a m 特 有 的 操 作 f s t r e a m   fstrm; 创建一个未绑定的文件流 f s t r e a m  是头文件 fstream 中定义的一个类型 f s t r e a m   fstrm(s); 创建一个  f s r e a m , 并打开名为 s 的文件 s 可以是 string 类型,或者是一个指向 C 风格字符串的指针 这些构造函数都是 explicit 的 默认的文件模式 mode 依赖于  f s t r e a m  的类型 f s t r e a m   fstrm(s, mode); 与前一个构造函数类似,但是会按照指定 mode 打开文件 fstrm.open(s) 打开名为 s 的文件,并将文件与 fstrm 绑定 fstrm.open(s, mode) 以指定模式 mode 打开名为 s 的文件,并将文件与 fstrm 绑定 s 可以是 string 类型,或者是一个指向 C 风格字符串的指针 默认的文件模式 mode 依赖于  f s t r e a m  的类型。返回 void fstrm.close() 关闭与 fstrm 绑定的文件。返回 void fstrm.is_open() 指出与 fstrm 关联的文件是够成功打开且尚未关闭。返回 bool 值 \begin{array}{c} \hline \bold{fstream 特有的操作}\\ \hline \begin{array}{l l} fstream\ \ \text{fstrm;} & \text{创建一个未绑定的文件流}\\ & fstream\ \text{是头文件 fstream 中定义的一个类型}\\ fstream\ \ \text{fstrm(s);} & \text{创建一个}\ fsream,\text{并打开名为 s 的文件}\\ & \text{s 可以是 string 类型,或者是一个指向 C 风格字符串的指针}\\ & \text{这些构造函数都是 explicit 的}\\ & \text{默认的文件模式 mode 依赖于}\ fstream\ \text{的类型}\\ fstream\ \ \text{fstrm(s, mode);} & \text{与前一个构造函数类似,但是会按照指定 mode 打开文件}\\ \text{fstrm.open(s)} & \text{打开名为 s 的文件,并将文件与 fstrm 绑定}\\ \text{fstrm.open(s, mode)} & \text{以指定模式 mode 打开名为 s 的文件,并将文件与 fstrm 绑定}\\ & \text{s 可以是 string 类型,或者是一个指向 C 风格字符串的指针}\\ & \text{默认的文件模式 mode 依赖于}\ fstream\ \text{的类型。返回 void}\\ \text{fstrm.close()} & \text{关闭与 fstrm 绑定的文件。返回 void}\\ \text{fstrm.is\_open()} & \text{指出与 fstrm 关联的文件是够成功打开且尚未关闭。返回 bool 值}\\ \end{array}\\ \hline \end{array} fstreamfstream  fstrm;fstream  fstrm(s);fstream  fstrm(s, mode);fstrm.open(s)fstrm.open(s, mode)fstrm.close()fstrm.is_open()创建一个未绑定的文件流fstream 是头文件 fstream 中定义的一个类型创建一个 fsream并打开名为 s 的文件可以是 string 类型,或者是一个指向 C 风格字符串的指针这些构造函数都是 explicit 默认的文件模式 mode 依赖于 fstream 的类型与前一个构造函数类似,但是会按照指定 mode 打开文件打开名为 s 的文件,并将文件与 fstrm 绑定以指定模式 mode 打开名为 s 的文件,并将文件与 fstrm 绑定可以是 string 类型,或者是一个指向 C 风格字符串的指针默认的文件模式 mode 依赖于 fstream 的类型。返回 void关闭与 fstrm 绑定的文件。返回 void指出与 fstrm 关联的文件是够成功打开且尚未关闭。返回 bool 

8.2.1 使用文件流对象

​ 创建文件流对象时,可以提供文件名(可选)。如果提供,则 open() 会自动被调用:

ifstream in(ifile);     // 构造一个 ifstream 并打开给定文件
ofstream out;           // 输出流未关联到任何文件

(1)用 fstream 代替 iostream&

​ 如果有一个函数接受一个 ostream& 的参数,我们也可以给他穿第一个 ofstream 对象。对 istream 和 ifstream 也是同理。因为:

  • ofstream 和 ostringstream 都继承自 ostream
  • ifstream 和 istringstream 都继承自 istream

(2)成员函数 open 和 close

​ 如果我们定义了一个空文件流对象,可以随后调用 open 来将它与文件关联起来:

ifstream in(ifile);           // 构筑一个 ifstream 并打开给定文件
ofstream out;                 // 输出文件流未与任何文件关联
out.open(ifile + ".copy");    // 打开指定文件

​ 如果调用 open() 失败,failbit 会被置位。因此进行 open() 是否成功的检测通常是一个好习惯:

if (out)       // 检查 open 是否成功,与 cin 用作条件类似
               // open 成功,我们可以使用文件了

​ 对一个已经打开的文件流调用 open() 会失败,导致 failbit 被置位,随后试图使用文件流的操作会失败。因此必须先关闭已经关联的文件,才可以打开新的文件:

in.close();              // 关闭文件
in.open(ifile + "2");    // 打开另一个文件

​ 如果 open() 成功,则 open() 会设置流的状态,使得 good() 为 true。

(3)自动构造和析构

​ 当一个 fstream 对象离开其作用域时(即被销毁),与之关联的文件会自动关闭,即 close() 会被自动调用。

8.2.2 文件模式

文 件 模 式 in 以读方式打开 out 以写方式打开 app 每次写操作前均定位到文件末尾 ate 打开文件后立即定位到文件末尾 trunc 截断文件 binary 以二进制方式进行 IO \begin{array}{c} \hline \bold{文件模式}\\ \hline \begin{array}{l l} \text{in} & \text{以读方式打开}\\ \text{out} & \text{以写方式打开}\\ \text{app} & \text{每次写操作前均定位到文件末尾}\\ \text{ate} & \text{打开文件后立即定位到文件末尾}\\ \text{trunc} & \text{截断文件}\\ \text{binary} & \text{以二进制方式进行 IO}\\ \end{array}\\ \hline \end{array} inoutappatetruncbinary以读方式打开以写方式打开每次写操作前均定位到文件末尾打开文件后立即定位到文件末尾截断文件以二进制方式进行 IO

​ 指定文件模式有如下限制:

  • 不能对 ifstream 对象设定 out 模式;不能对 ofstream 对象设定 in 模式
  • 只有当 out 被设定时,才可以设定 trunc 模式
  • 没有设定 trunc 时,才可以设定 app 模式。app 模式默认了 out 模式,文件以输出方式打开
  • 默认情况下,即使没有指定 trunc 模式,以 out 模式打开的文件也会被截断。为了保留文件内容:
    • 同时设定 app 模式,这样会将数据追加到文件末尾
    • 同时设定 in 模式(仅对 fstram 对象有效),进行同时读写操作
  • ate 和 binary 模式可用于任何类型的文件流对象,并且可以与其他任何文件模式组合使用
  • 未指定文件模式时:
    • ifstream 默认使用 out 模式
    • ofstream 默认使用 in 模式
    • fstream 默认使用 in 和 out 模式

(1)以 out 模式打开文件会丢弃已有数据

​ 默认情况下,当我们打开一个 ofstream 时,文件的内容会被丢弃。阻止一个 ofstream 清空给定文件内容的方法是同时指定 app 模式:

// 在这几条语句中,file1 都被截断
ofstream out("file1");    // 隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out);    // 隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);

// 为了保留文件内容,我们必须显式指定 app 模式
ofstream app("file2", ofstream::app);
ofstream app("file2", ofstream::out | ofstream::app);

​ 保留被 ofstream 打开的文件中已有数据的唯一方法是显式指定 app 或 in 模式。

(2)每次调用 open 时都会确定文件模式

​ 对于一个给定流,每当打开文件时,都可以改变其文件模式。

ofstream out;             // 未指定文件打开模式
out.open("scratchpad");   // 模式隐含设置为输出和截断
out.close();              // 关闭 out,以便我们将其用于其他文件
out.open("precious", ofstream::app);   // 模式为输出和追加 
out.close();

​ 在每次打开文件时,都要设置文件模式,可能是显式地设置,也可能是隐式地设置。当程序未指定模式时,就使用默认值。

8.3 string 流

stringstream 特有的操作 s s t r e a m   strm; strm 是一个未绑定的 stringstream 对象 s s t r e a m   是头文件 sstream 中定义的一个类型 s s t r e a m   strm(s); strm 是一个  s s t r e a m  对象,保存 string s 的一个拷贝 此构造函数是 explicit 的 strm.str() 返回 strm 所保存的 string 的拷贝 strm.str(s) 将 string s 拷贝到 strm 中。返回 void \begin{array}{c} \hline \hline \text{stringstream 特有的操作}\\ \hline \begin{array}{l l} sstream\ \ \text{strm;} & \text{strm 是一个未绑定的 stringstream 对象}\\ & sstream\ \ \text{是头文件 sstream 中定义的一个类型}\\ sstream\ \ \text{strm(s);} & \text{strm 是一个}\ sstream\ \text{对象,保存 string s 的一个拷贝}\\ & \text{此构造函数是 explicit 的}\\ \text{strm.str()} & \text{返回 strm 所保存的 string 的拷贝}\\ \text{strm.str(s)} & \text{将 string s 拷贝到 strm 中。返回 void} \end{array}\\ \hline \end{array} stringstream 特有的操作sstream  strm;sstream  strm(s);strm.str()strm.str(s)strm 是一个未绑定的 stringstream 对象sstream  是头文件 sstream 中定义的一个类型strm 是一个 sstream 对象,保存 string s 的一个拷贝此构造函数是 explicit 返回 strm 所保存的 string 的拷贝 string s 拷贝到 strm 中。返回 void

8.3.1 使用 istringstream

​ 考虑这样一个例子,假定有一个文件,列出了一些人和他们的电话号码。某些人只有一个号码,而另一些人则有多个——家庭电话、工作电话、移动电话等。我们的输入文件看起来可能是这样的:

​ morgan 2015552368 8625550123

​ drew 9735550130

​ lee 6095550132 2015550175 8005550000

​ 我们首先定义一个简单的类来描述输入数据:

// 成员默认为共有
struct PersonInfo {
    string name;
    vector<string> phones;
}

​ 我们的程序会读取数据文件,并创建一个 PersonInfo 的 vector。vector 中每个元素对应文件中的一条记录。我们在一个循环中处理输入数据,每个循环步读取一条记录,提取出一个人名和若干电话号码:

string line, word;                    // 分别保存来自输入的一行和单词
vector<PersonInfo> people;            // 保存来自输入的所有记录

// 逐行从输入读取数据,直至 cin 遇到文件尾(或其他错误)
while (getline(cin, line)) {          
    PersonInfo info;                  // 创建一个保存此记录数据的对象
    istringstream record(line);       // 将记录绑定到刚读入的行
    record >> info.name;              // 读取名字
    while (record >> word)            // 读取电话号码
        info.phones.push back(word);  // 保持它们
    people.push_back(info);           // 将此记录追加到 people 末尾
}

​ 这里我们用 getline 从标准输入读取整条记录。如果 getline 调用成功,那么 line 中将保存着从输入文件而来的一条记录。

​ 接下来我们将一个 istringstream 与刚刚读取的文本行进行绑定。我们首先读取人名,随后用一个 while 循环读取此人的电话号码。当读取完 1ine 中所有数据后,内层 while 循环就结束了。此循环的工作方式与前面章节中读取 cin 的循环很相似,不同之处是,此循环从一个 string 而不是标准输入读取数据。当 string 中的数据全部读出后,同样会触发“文件结束”信号,在 record 上的下一个输入操作会失败

​ 我们将刚刚处理好的 PersonInfo 追加到 vector 中,外层 while 循环的一个循环步就随之结束了。外层 while 循环会持续执行,直至遇到 cin 的文件结束标识。

8.3.2 使用 ostringstream

​ 对上一节的例子,我们可能想逐个验证电话号码并改变其格式。如果所有号码都是有效的,我们希望输出一个新的文件,包含改变格式后的号码。对于那些无效的号码,我们不会将它们输出到新文件中,而是打印一条包含人名和无效号码的错误信息。

​ 由于我们不希望输出有无效电话号码的人,因此对每个人,直到验证完所有电话号码后才可以进行输出操作。但是,我们可以先将输出内容“写入”到一个内存 ostringstream 中:

for (const auto &entry : people) {             // 对 people 中每一项
    ostringstream formatted, badNums;          // 每个循环步创建的对象
    for (const auto &nums : entry.phones) {    // 对每个数
        if (!valid (nums))
            badNums << " " << nums;            // 将数的字符串形式存入 badNums 
        else
            formatted << " " << format(nums);  // 将格式化的字符串“写入” formatted 
    }
    if (badNums.str().empty())                 // 没有错误的数
        os << entry.name << " "                // 打印名字
           << formatted.str() << endl;         // 和格式化的数
    else                                       // 否则,打印名字和错误的数
        cerr << "input error: " << entry.name
             << " invalid number(s) "<< badNums.str() << endl;
}

​ 在此程序中,我们假定已有两个函数,valid() 和 format(),分别完成电话号码验证和改变格式的功能。程序最有趣的部分是对字符串流 formatted 和 badNums 的使用。我们使用标准的输出运算符(<<)向这些对象写入数据,但这些“写入”操作实际上转换为 string 操作,分别向 formatted 和 badNums 中的 string 对象添加字符。