第八章 IO 库

52 阅读9分钟

C++ 通过一组定义在标准库中的类型来处理 IO。这些类型支持读写设备数据的 IO 操作,设备可以是文件、控制台窗口等。还有一些类型允许内存 IO,比如,对 string 读写数据。IO 库定义了读写内置类型的操作。有些类,像 string,也会定义类似的 IO 操作,来读写自己的对象。

IO 类

为了支持不同种类的 IO 操作,标准库定义了一些 IO 类型:

  • iostream:定义了用于读写流的基本类型。
  • fstream:定义了读写命名文件的类型。
  • sstream:定义了读写内存 string 对象的类型。

IO-type-h.png

为支持宽字符,标准库定义了一组类型和对象来操纵 wchar_t 类型数据。宽字符版本的类型和函数名字以 w 开头。宽字符版本的类型和对象与其对应的 char 版本的类型定义在同一头文件中。

概念上,设备类型(比如控制台窗口、磁盘文件、内存 string)和字符大小(比如 charwchar_t)不会影响要执行的 IO 操作。标准库通过继承机制(inheritance)让我们忽略不同类型的流之间的差异。在继承关系中,可以将一个派生类对象当做基类对象来使用,比如:ifstreamistringstream 继承自 istream,因此可以对 ifstreamistringstream 对象调用 getline>>

不能对 IO 对象拷贝或赋值,因此,不能将形参或返回类型设置为流类型。进行 IO 操作的函数通常以非常量引用方式传递或返回流。

条件状态

IO 操作的一个与生俱来的问题是可能引发错误。一些错误可恢复,其它错误则发生在系统深处,超出应用程序可修正的范围。IO 类定义了一些函数和标志,用于访问和操纵流的条件状态(condition state)。

condition-state-1.png condition-state-2.png

流一旦发生错误,它后续的 IO 操作都会失败。使用流之前通常应检查它是否处于良好状态。确定流对象状态的最简单的方法是将其作为一个条件来使用。

while (cin >> word)
  // ...

流作为条件只能知道是否有效,但无法知道失败原因。IO 库定义了一个机器相关的 iostate 类型,它提供了表达流状态的完整功能,该类型应作为一个位集合来使用。IO 库定义了 4 个 iostate 类型的 constexpr 值表示特定的位模式。这些值用来表示特定类型的 IO 条件,可与位运算符一起使用,来一次性检测或设置多个标志位。

  • badbit:系统级错误,如:不可恢复的读写错误。通常情况下,一旦置位,流无法使用。
  • failbit:可恢复错误,如:期望读取数值但读取一个字符。通常可修正,流可以继续使用。
  • eofbit:到达文件结束位置,failbit 也会置位。
  • goodbit:未发生错误,值为 0。

badbitfailbiteofbit 任一被置位,检测流状态的条件都会失败。标准库还定义了一组函数来查询标志位的状态:

  • good:所有错误均未置位时返回 true
  • bad/fail/eof:对应错误位被置位时返回 true
  • 如果 badbit 被置位,fail 也为 true

使用 goodfail 是确定流总体状态的正确方法。流作为条件使用的代码等价于 !fail()eofbad 只能表示特定错误。

以下函数可以操纵条件状态:

  • setstate:将入参中置 1 的位所对应的条件位置位,表示发生了对应错误。
  • 无参数 clear:清除(复位)所有错误标志位。
  • 形参类型为 iostateclear:根据实参将流复位,实参表示流的新状态。
auto old_state = cin.rdstate();
cin.clear();
process(cin);
cin.setstate(old_state);

管理输出缓冲

每个输出流都管理一个缓冲区,用于保存程序读写的数据。缓冲机制让操作系统可以将程序的多个输出操作合成单个系统级写操作。由于设备的写操作可能很耗时,因此这种写操作合成的做法可以带来很大的性能提升。

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

  • 程序正常结束,作为 main 函数 return 操作的一部分,执行缓冲刷新。
  • 缓冲区满,此时刷新缓冲,新数据才能继续写入缓冲区。
  • 操纵符 endl 显式刷新缓冲区。
  • 操纵符 unitbuf 设置流的内部状态,在每个输出操作之后清空缓冲区;cerr 默认设置 unitbuf
  • 一个输出流 os 可能被另一个流 s 所关联。此时,若读写 sos 的缓冲区会被刷新;cin/cerr 默认关联到 cout

刷新输出缓冲区的操纵符:

  • endl:换行并刷新缓冲区。
  • flush:刷新缓冲区,不输出任何额外字符。
  • ends:插入空字符并刷新缓冲区。

unitbuf 操纵符让流在接下来的每次写操作之后都进行一次 flush 操作。nounitbuf 操纵符将流重置,使其恢复使用正常的系统管理的缓冲区刷新机制。

cout << unitbuf;
cout << nounitbuf;

如果程序异常终止,输出缓冲区不会刷新,所输出的数据很可能停留在输出缓冲区中等待打印。因此,调试程序时,需要确认数据确实已经刷新,否则可能浪费大量时间。

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

tie 成员有两个重载版本:

  • 无参数:返回一个指向所关联输出流的指针,如果没有,则返回空指针。
  • 形参类型为指向 ostream 的指针:返回一个指向先前所关联的输出流的指针,并将自身关联到实参所指向的新的 ostream
cin.tie(&cout);
ostream *old = cin.tie(nullptr);
cin.tie(&cerr);
cin.tie(old);

文件输入输出

头文件 fstream 定义了三个类型来支持文件 IO:

  • ifstream:从给定文件读取数据。
  • ofstream:向给定文件写入数据。
  • fstream:可读写给定文件。

除了继承 iostream 类型的行为之外,fstream 中定义的类型还增加了新的成员来管理与流关联的文件。

fstream.png

使用文件流对象

要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。每个文件流类都定义了一个名为 open 的成员函数,它完成了一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。创建文件流对象时,如果提供了一个文件名,则 open 会自动调用;若无参数,则不与任何文件关联。

由于类之间的继承关系,接收 iostream 类型引用(指针)参数的函数,可以用 fstream(或 stringstream)类型来调用。

成员函数 openclose

空文件流对象可以调用 open 来和文件关联起来。如果 open 失败,failbit 将被置位。文件流一旦打开,就会保持与对应文件的关联。对一个已打开的文件流调用 open 会失败,并将 failbit 置位,随后使用该文件流的操作都会失败。将文件流关联到另一个文件时,必须先关闭已关联的文件。

ifstream in(ifile);
ofstream out;
out.open(ifile + ".copy");

if (out) {
  // ...
}

in.close();
in.open(ifile + "2");

自动构造与析构

局部变量在整个生命周期中要经历一次创建和销毁。fstream 对象被销毁时,close 会自动调用。

for (auto p = argv + 1; p != argv + argc; ++p) {
  ifstream input(*p);
  if (input) {
    process(input);
  } else {
    cerr << "couldn't open: " + string(*p);
  }
}

文件模式

每个流都有一个关联的文件模式(file mode),用来指出如何使用文件。

file-mode.png

无论以哪种方式打开文件(调用 open 打开文件、或用文件名初始化流来隐式打开文件),都可以指定文件模式。指定文件模式有如下限制:

  • 只能对 ofstreamfstream 对象设定 out 模式。
  • 只能对 ifstreamfstream 对象设定 in 模式。
  • 只有设定了 out 时才能设定 trunc 模式。
  • 只要没设定 trunc,就可以设定 app 模式。app 模式下,即使没有显式设定 out 模式,文件也总以输出方式打开。
  • 即使没有指定 trunc,默认情况下,以 out 模式打开的文件也会被截断。要保留文件内容,必须同时指定 app 模式——将数据追加到文件末尾,或者同时指定 in 模式——打开文件同时进行读写操作。
  • atebinary 模式可用于任何类型的文件流对象,可与其它任何文件模式组合使用。

每个文件流类型都有默认的文件模式:

  • ifstream 关联的文件默认以 in 模式打开。
  • ofstream 关联的文件默认以 in 模式打开。
  • fstream 关联的文件默认以 inout 模式打开。

保留被 ofstream 打开的文件中数据的唯一方法就是显式指定 appin

// 以下三种打开方式都会截断数据
ofstream out("file1");
ofstream out2("file1", ofstream::out);
ofstream out3("file1", ofstream::out | ofstream::trunc);

// 以下打开方式会保留文件内容
ofstream app("file2", ofstream::app); // 隐式 out
ofstream app2("file2", ofstream::out | ofstream::app);

每次打开文件时,都要设置文件模式,可以显式设置,也可以隐式设置。未指定模式时,使用默认值。

ofstream out;
out.open("scratchpad");
out.close();
out.open("precious", ofstream::app);
out.close();

string

sstream 头文件定义了三个类型支持内存 IO:

  • istringstream:从 string 读取数据。
  • ostringstream:向 string 写入数据。
  • stringstream:向 string 读写数据。

除了从 iostream 中继承的操作之外,sstream 中定义的类型还增加了一些成员来管理与流相关联的 string

stringstream.png

如果既需要处理整行文本,又需要处理行内的单个单词,则通常可使用 istringstream。如果需要逐步构造输出,最后一起打印,那么 ostringstream 很有用。