C++ Primer:IO 类

C++ Primer:IO 类

在Linux中,OS 会为我们提供两个系统调用 read write ,通过这两个系统调用,就能够完成对文件的读写。由于 Linux 万物皆文件的特点,因此 read / write 同样能够读写控制台、网络socket。通过 read/write 就能够与用户在 标准输入输出 中交互。

但是,由于没有缓冲区, write/read 的效率在很多情况下效率是很低的。为此,C++ 实现了 标准IO库 对文件、控制台进行读写,它不仅调用了 read/write ,而且又有自己的缓冲区,能够加大 IO 的效率。

C++ 标准IO 的强大远不止此,在本文中将会介绍C++ 标准IO库如何对 控制台、文件、字符串进行读写。

1. The IO Classes

在C++中,经常会使用 cin cout 等与控制台进行交互。在实际的系统,程序不仅要与控制台交互,程序还要读写文件。C++ 的标准IO库中,还可以对字符串当作一个文件进行读写。由于字符串作为程序的一部分保存在虚拟内存中,并不是每次都要IO操作,可以认为是对内存进行读写。



流(stream) 是一个向文件、string读写的字符串序列,我们可以抽象的认为一个打开的文件就是一个流,在 C++ 中对于流有三个头文件:

  • iostream :定义了流的基本类型,能够对控制台读写
  • fstream :定义了用于读写文件的流
  • sstream :定义了用于读写 string 的流

为了支持宽字符,C++ 还实现了对宽字符进行读写的流,如图,例如 wcin wistream 就是 cin istream 的宽字符版本,功能上大抵相同。

以输入流为例, istream ifstream sstream 的基类,后面两个流都是 istream 的子类,继承了 istream 的特性。也就是说:

  • istream 可以通过多态转化为 fstream sstream
  • fstream sstream 都继承或重写了 istream 的一些操作符,比如对于这两种流都可以使用 << >> 。其中 << 向流输出、 >> 从流输入。
std::fstream test_stream;
test_stream.open(...);
test_stream << "...";


1.1 No Copy or Assign for IO Objects

在 C++中,为了避免多个IO对象指向同一个文件进而导致一些深拷贝、内存泄漏等问题,C++ 阻止了对于 istream 对象的拷贝,同时由于 fstream sstream 继承了 istream 对象,因此这两个对象也都是无法拷贝的。也是就是 IO 对象是无法复制的

ofstream out1, out2;
out1 = out2; // error: cannot assign stream objects
ofstream print(ofstream); // error: can't initialize the ofstream parameter
out2 = print(out2); // error: cannot copy stream objects
/* C++ 11 可以使用 std::move 改变所有权 */
out1 = std::move(out2);

假如使用了拷贝构造、等号赋值、或者将 IO 对象作为参数传入函数中,将 IO 对象作为返回值,都会出错。如果要将 IO对象作为参数和返回值,就需要定义为 指针类型 引用类型 。在引用时,在函数中进行 write read 可能会修改流的状态,因此不要加上 const

std::ifstream &test(std::ifstream& a) {
    return a;
std::ifstream &is = test(ios);
// ...
std::ifstream *test(std::ifstream* a) {
    return a;
std::ifstream *is = test(ios);

1.2 Condition States





在 OS 中,IO 操作可能因为莫名其妙的原因发生错误,这可能不是用户的锅,但是为了防止出现错误后能够更好的排查,并且不带来其他影响,用户有时候就需要获得 IO 的条件状态。当进行 IO 操作时,如果发生了错误,流的一些条件状态就会被设置,初始状态是 gootbit (0),设置时会通过 | 运算改变条件状态。

  • goodbit :初始值,为0
  • failbit :io操作发生了一些错误,通常是可恢复的。比如 x 是int类型,输入时输入了一个字符 ‘a’ ,因为 int 类型不匹配 'a' 就会被设置。如果 failbit 被设置,流在清楚 failtbit 后,还是可以继续使用。
  • badbit :发生了系统级别的错误,通常是不可以恢复的。如果 badbit 被设置,这个流就不能再使用了。
  • eofbit :当一个文件读到末尾时, eofbit failbit 都会被设置。通过 seek 改变文件位置,这个流还是可以继续使用。

可以使用上图中的函数得到对应的bit是否被设置,特殊的是,如果 badbit 被设置了,调用 fail() 将会返回 true 。因此,通常读入一个文件时可以如下所示:

std::fstream fs;
fs.open(...);
while(fs >> x >> y) {

由于 EOF 发生时,会设置 eofbit failbit bad 发生时,调用 fail() 将会返回 true 。因此,上面的循环判断条件等价于: while(!fs.fail()) ,当发生 eof fail bad 都会终止循环条件。




由于流在使用之前,会查看自己的条件状态是不是 goodbit 如果不是 goodbit 的话,就不会使用这个流 。因此,假如三个状态被设置了一个,这个流都不能使用。

while(std::cin>>x>>y) {
        std::cout << x << ' ' << y << '\n';
    std::cout << x << ' ' << y << '\n';
如上,输入 

上面的例子说明,一旦流发生了错误,后面一个 << 操作进入流时,首先检查当前流的状态,如果发现不是 goodbit ,就会立即退出。因此 y 最终并没有被修改。

但是 failbit 被设置了,是可以恢复的,这是怎么实现的呢?如上图中,我们可以通过 clear 来清除某个位、全部位。或者可以通过 setstate 来将状态设置为 goodbit

// remember the current state of cin
auto old_state = cin.rdstate(); // remember the current state of cin
cin.clear(); // make cin valid
process_input(cin); // use cin
cin.setstate(old_state); // now reset cin to its old state
// turns off failbit and badbit but all other bits unchanged
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);

通过 clear setstate ,我们就能够设置条件状态,获得我们想要的结果。

1.3 Managing the Output Buffer

每个输入输出流都会有自己的缓冲区,用来减少 read write 系统调用的消耗。

os << "please enter a value: ";

以写操作为例,例如上面这个输出操作后,他立即就写到对应的文件了吗?其实不然,一般缓冲区在你不告诉他要立即写到对应的文件时,他就很不老实。你做的每次写操作都会先写到它的缓冲区上,等到缓冲区满了,他才会把缓冲区的内容更新到对应的文件上。

这是系统做的一个优化,因为每次流操作都要更新缓冲区的话,IO 代价会特别大。但是一些情况下,你可能特别希望没有缓冲区存在,让数据立即更新到文件上。譬如说:日志文件通过是要实时更新的。

还有一种情况就是,执行到一半的时候,程序崩溃了,那么此时缓冲区的内容...并没有更新到磁盘的文件上。如果你没有实时刷新缓冲区,不断执行这个会崩溃的程序时,由于缓冲区的存在,每次打印出来的内容可能都是不一样的。这对于程序员来说, 很难找到程序崩溃的原因,不利于 debugging




明白了为什么要刷新缓冲区,下面怎么刷新缓冲区、什么时候刷新缓冲区?(注意这里的缓冲区指的是写缓冲区 cout )

  • 程序结束时, main 函数返回,会自动刷新 ouput 缓冲区
  • 写缓冲区满了,在下次写操作之前就会刷新缓冲区
  • 通过特定的操作符 endl ends flush 可以手动刷新缓冲区
  • 通过 unitbuf 操作符设置流的内部状态,每次写操作后都会刷新缓冲区。 cerr 默认情况下自动设置 unitbuf
  • 一个输出流和其他流关联。此时读写被关联的流(不是输出流的其他流),关联到的输出流的缓冲区会被刷新。 默认情况下 cin cerr 都关联到 cout ,因此读 cin 或写 cerr 都会刷新 cout 缓冲区
    • 其他流可以是 istream 或者 ostream ,但是一定关联一个 ostream


1.3.1 Flushing the Output Buffer

cout << "hi!" << endl; // writes hi and adds a newline, then flushes the buffer
cout << "hi!" << flush; // writes hi, then flushes the buffer; adds no data
cout << "hi!" << ends; // writes hi and a null, then flushes the buffer

1.3.2 The unitbuf Manipulator

如果每次写操作后,都想立即刷新缓冲区,可以使用 unitbuf 。他会告诉流对后面的每个写操作都立即刷新。

cout << unitbuf; // all writes will be flushed immediately
// any output is flushed immediately, no buffering
cout << nounitbuf; // returns to normal buffering

1.3.3 Tying Input and Output Streams Together

当一个流和一个输出流关联,那么每次读这个流,都会刷新输出流的缓冲区。

默认情况下,IO 库会自动关联 cin cout 。也就是说,每次 cin >> val ,就会使得 cout 的缓冲区立即刷新。在交互系统,例如控制台下,每个 cin 前都会刷新 cout 缓冲区,输入一个提示语句到控制台下。这么做就意味着在每次 cin 之前,提示语句一定会先出现。

tie 函数会有两个重载版本:

  • 无参数版本 :返回一个指针,指向关联的输出流。如果这个流关联了输出流,返回输出流的指针。如果这个流没关联输出流,返回空指针。
  • 有参数版本 :参数是一个 ostream 的指针。将当前流关联到 ostream
cin.tie(&cout); // illustration only: the library ties cin and cout for us
// old_tie points to the stream (if any) currently tied to cin
ostream *old_tie = cin.tie(nullptr); // cin is no longer tied
// ties cin and cerr; not a good idea because cin should be tied to cout
cin.tie(&cerr); // reading cin flushes cerr, not cout
// restore
cin.tie(old_tie); // reestablish normal tie between cin and cout

首先我们会将 cout cin 关联起来。然后通过无参版本的 cin.tie cout 就不再和 cin 关联,也就是说每次读 cin cout 不会刷新缓冲区。后面再手动将 cerr cin 关联,此时每次读 cin cerr 都会立即刷新。

每个流同时最多关联一个输出流,但是多个流可以同时关联到同一个 ostream 。比如 cin cerr 能同时关联 cout

2. File Input and Output

头文件 fstream 当中定义了三个类型,支持文件IO:

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



fstream 不仅继承了 stream 的函数,而且还有额外的几个新函数,用来管理与流关联的文件。

2.1 Using File Stream Objects

当读写一个文件时,可以定义一个文件流对象,让 对象 文件 关联起来。 fstream 流实现了一个 open 函数,基于系统调用 open ,C++ 会自动关联对应的文件。假如我们将一个 文件名 传送给构造函数, fstream 对象就会自动调用 open 函数。

ifstream in(ifile); // construct an ifstream and open the given file
ofstream out; // output file stream that is not associated with any file

如图,会创建一个输入流 in, infile 是一个文件名(字符串), in 会自动打开文件 infile

2.1.1 Using an fstream in Place of an iostream&

假如函数的形参是一个父类,那么如果我们传入的实参是子类。由于多态,也是不影响使用的。在 IO 类中,假如父类是 istream ,子类是 ifstream ,同样可以验证多态的思想。

int get(std::istream &x) {
    return  1;
int main() {
    std::ifstream x;
    std::cout << get(x);
    return 0;

2.1.2 The open and close Members

假如定义 fstream 对象时,使用了无参构造器,没有关联对应的文件,那么之后使用这个对象就需要手动 open 打开对应的文件。

ifstream in(ifile); // construct an ifstreamand open the given file
ofstream out; // output file stream that is not associated with any file
out.open(ifile + ".copy"); // open the specified file
if(out.isopen()) {
// 或者
if(out) {

如果 open 失败, failbit 会被设置,因此可以在 open 之后可以检测文件是否成功打开。一旦文件流被打开,他就会保持与对应文件的关联。对于一个已经打开的文件调用 open 通常会出错,由于 failbit 被设置,后面的所有 io 操作都会失败,所以 及时判断文件是否打开是一个很好的习惯

对于一个局部 fstream 对象,当他离开作用域时,就会自动调用析构函数,析构函数中包含 close() 函数。但是对于指针分配的 fstream 对象,作用域可能不明显,C++有时候不会及时的收回分配的内存。此时如果关联的文件不再使用,应该用 close 及时关闭,防止打开文件过多,内存泄漏。

2.2 File Modes



当每个文件打开时,可以设置一个打开的 文件模式 ,用来指出如何使用文件。无论用哪种方式打开文件,都可以指定文件模式(构造函数,open显示打开)。文件模式有以下限制:

  • 只可以对 fstream 和 ifstream 对象设定 out 模式
  • 只可以对 ifstream 或 fstream 对象设定 in 模式
  • 只有当 out 被设置才能设置 trunc
  • 只要trunc没被设置,就可以设置 app 模式
  • 默认情况下,即使我们没有指定 trunc,以 out 模式打开的文件也会被截断。为了保留以 out 模式打开的文件的内容,必须指定 app 模式,但是这只会让数据追加到文件末尾;或者可以同时指定 in 模式。
  • ate、binary 模式可用于任何类型的文件流对象,也可以组合使用

每个文件流类型都有一个默认的文件模式,比如 fstream 默认读写权限。

如果没有 in 和 app 模式,每次打开文件都相当于创建一个新文件

2.2.1 Opening a File in out Mode Discards Existing Data

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

// file1 is truncated in each of these cases
ofstream out("file1"); // out and trunc are implicit
ofstream out2("file1", ofstream::out); // trunc is implicit
ofstream out3("file1", ofstream::out | ofstream::trunc);
// to preserve the file's contents, we must explicitly specify app mode
ofstream app("file2", ofstream::app); // out is implicit
ofstream app2("file2", ofstream::out | ofstream::app);