1. 概述

为了概览一下C++的IO库,先来张cplusplus.com网站的图片:

iostream

从这个图片我们基本上可以得到C++ IO库的所有基本信息:头文件、类、继承关系。为了更清楚的看出各个类的继承关系,我根据上面的图重新画了一个:

io

从上面两个图可以看出,C++的IO都是流式的,主要分为三大类:普通流文件流字符串(string)流。这三大类的流各有各的特点,但也有许多共性,我们先来看一些共性。

1.1 条件状态

其实所谓的条件状态很容易理解,就是标识流此刻的状态。条件状态(均为ios_base::iostate类型,实质是一种枚举类型,定义在ios_base.h头文件中)共有四种:

状态 条件状态检测函数 状态说明
badbit s.bad() 用于指出被破坏的流,不可恢复
eofbit s.eof() 用于指出流已经到达文件结束符
failbit s.fail() 用于指出失败的IO操作
goodbit s.good() 用于指出流是OK的

除了上面四个条件状态检测函数,还有几个操作条件状态的函数:

函数名 函数作用
s.clear() 将流s的状态设置为goodbit状态
s.clear(flag) 将流s的状态设置为flag状态,flag的类型是ios_base::iostate类型
s.setstate(flag) 给流s添加指定条件
s.rdstate() 返回流s的当前条件,返回值类型为ios_base::iostate类型

关于条件状态的使用,看下面几个代码片段:

#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>

using namespace std;

int main()
{
	int ival;
	while (cin >> ival, !cin.eof())
	{
		if (cin.bad())
			throw runtime_error("IO stream corrupted");
		if(cin.fail())
		{
			cerr << "bad data, try again" << endl;
			cin.clear(istream::failbit);
			continue;
		}
		cout << "ival:" << ival << endl;
	}

	return 0;
}

这段代码是《C++ Primer》第四版上面的一个例子,该段代码的作用是循环不断读入cin,直到到达文件结束符或者发生不可恢复的读取错误为止。但是这个例子是有问题的,具体见我之前的博客《关于<C++ Primer>中cin.fail可能造成死循环的问题分析》。

2.2 刷新缓冲区

缓冲区和IO就像孪生兄弟一样,有IO的地方,必然不能少了IO。说实话,我对C++的缓冲区还没有研究过,这里就拿C的缓冲区来说吧(我想应该差不了多少)。在标准C中,共有三种类型的缓冲:

(1)全缓冲(fully buffered)——只有填满缓冲区候才进行实际的I/O操作。这类缓冲通常用在文件操作中。

(2) 行缓冲(line-buffered)——当输入或输出中遇到换行符时才进行实际的I/O操作。键盘输入就是标准的行缓冲。

(3)无缓冲(non-buffered)——即没有缓冲。标准C的I/O库不对字符进行缓冲存储。

言归正传,这里我们的重点不是讲缓冲区的概念及类型,而是看在C++中,哪些情况可以刷新缓冲区(即数据真正的写到设备或文件):

  • 程序正常结束。当程序正常从main函数return时,所有的输出缓冲会被刷新。
  • 缓冲区满了以后,缓冲区也会被刷新。
  • 我们使用一些强制刷新缓冲区的操作。比如:(1)endl——插入换行符,然后刷新缓冲区。(2)ends——插入一个null,然后刷新缓冲区。(3)flush——什么也不加,直接刷新缓冲区。
  • 使用unitbuf操作符。该操作符告诉流没进行一次连续的输出后,就刷新缓冲区。使用nounitbuf可以复原系统默认设置。
  • 当我们把输出流和另外一个流绑定在一起的时候,后者的读或写之前都会刷新前者的缓冲区。比如标准输出流cout默认是和cin与cerr绑定在一起的,这样cin读或者cerr写都会刷新cout流。这个是很重要的,比如讲cout与cin绑定在一起就可以保证我们每次读之前输出缓冲区已经被刷新了——例如,一般我们的程序在获取输入前都会输出一段提示,如果此时不先刷新输出缓冲区的话,该缓冲区内的内容就可能被当成输入的一部分。当然,一个流一次最多只能另外绑定一个流。

最后,需要特别注意的是,当程序崩溃、异常终止时,缓冲区并不会被刷新。最常见的例子就是,我们的程序在一些异常场景下打了日志,但这些日志还处于缓冲区时程序突然异常终止了,那此时缓冲区里面的日志并不会打印出来。这是我们需要注意的。

2.3 其他

(1)所有的IO对象都是不能拷贝和赋值的

ofstream out1, out2;
out1 = out2;				// 错误:不能给IO类型赋值
ofstream print(ofstream);	// 错误:不能拷贝
out2 = print(out2);			// 错误:不能拷贝

因为IO类型不能拷贝,所以函数的参数和返回值也不能是IO类型,只能使用引用类型。

(2)读写IO对象都会改变对象的状态,所以IO的引用也不能是const的

2. 文件流

文件IO类型都定义在fstream头文件中,共有三类:ifstream(读文件)、ofstream(写文件)、fstream(读写文件)。fstream将ifstream和ofstream的功能合并在一起,所以这里直接介绍fstream。由上面的继承关系可以看出fstream继承自iostream,所以它拥有iostream的特性,同时它还有自己特有(其他IO类型没有)的一些操作:

fstream fstrm 创建一个未和任何文件绑定的文件流
fstream fstrm(s) 创建一个和文件s绑定的文件流,s可以是string或char*;打开文件的模式取决于默认值
fstrean fstrm(s, mode) 和上面的一样,但指定打开文件的模式
fstrm.open(s)
fstrm.open(s,mode)
两个函数都是打开文件s,并将其与文件流fstrm绑定在一起。前者使用流默认的文件模式,后者使用指定的模式,返回void
fstrm.close( ) 关闭fstrm流绑定的文件,返回void。当fstrm被析构时,也会自动调用close
fstrm.is_open( ) 检查与文件流fstrm绑定的文件是否成功打开或者有没有被关闭

C++中的文件模式有以下几种:

in 用于打开一个输入流,ifstream流的默认值,ofstream不能使用该模式
out 用于打开一个输出流,oftream流的默认值,ifstream不能使用该模式
app 以追加写的方式打开一个输出流
ate 打开文件后,立刻定位到文件尾,可用于任何文件流
trunc 截断文件,即清空已有内容,仅用于输出流,且不可和app同时使用
binary 以二进制模式操作,可用于任何文件流
注意:默认情况下,输出流都是以trunc模式打开的,除非显式指定为app模式。

3. string流

sstream头文件中定义了三种用于操作“内存IO”的类型:istringstream(读string)、ostringstream(写string)、stringstream(读写string)。这里我们要注意一下,虽然这里我们把string流划分到IO去了,但其实它们的操作对象其实都是在内存中的string对象,并非平常说的那种IO。和文件流一样,string流继承自iostream,所以它也有iostream的特性,也有它自己特有的一些操作,这里以stringstream为例:

sstream strm 创建一个未和任何string类型绑定的stringstream对象。sstream是sstream头文件中定义的一种类型
sstream strm(s) 创建一个sstream流,该流与s的拷贝绑定
strm.str( ) 返回与strm流绑定的string
strm.str(s) 拷贝string s到流strm,返回void

4. sstream和strstream

上面我们介绍了sstream头文件里面包含的string流,其实还有一个strstream流,在strstream头文件中定义。它和sstream特别像,但是sstream是基于string实现的,而strstream是基于char*实现的。而strstream已经逐渐被废弃了,我们最好也不要再使用了,因为它非常的不安全。gcc的strstream头文件中是这样说的(其实是它包含的backward_warning.h中说的):

This file includes at least one deprecated or antiquated header which
may be removed without further notice at a future date. Please use a
non-deprecated interface with equivalent functionality instead. For a
listing of replacement headers and interfaces, consult the file
backward_warning.h. To disable this warning use -Wno-deprecated.

/*
A list of valid replacements is as follows:

Use:                                  Instead of:
<sstream>, basic_stringbuf            <strstream>, strstreambuf
<sstream>, basic_istringstream        <strstream>, istrstream
<sstream>, basic_ostringstream        <strstream>, ostrstream
<sstream>, basic_stringstream         <strstream>, strstream
<unordered_set>, unordered_set        <ext/hash_set>, hash_set
<unordered_set>, unordered_multiset   <ext/hash_set>, hash_multiset
<unordered_map>, unordered_map        <ext/hash_map>, hash_map
<unordered_map>, unordered_multimap   <ext/hash_map>, hash_multimap
<functional>, bind                    <functional>, binder1st
<functional>, bind                    <functional>, binder2nd
<functional>, bind                    <functional>, bind1st
<functional>, bind                    <functional>, bind2nd
<memory>, unique_ptr                  <memory>, auto_ptr
*/

这里列举出了旧的C++中存在的一些类型,后面会逐渐废弃的类型,后续我们也要避免使用。这里举一个例子:

/*
 * main.cpp
 *
 *  Created on: 2015年10月26日
 *      Author: Allan
 */

#include <iostream>
#include <sstream>
#include <string>
#include <strstream>

using namespace std;

string int2string_v1(int i)
{
	ostringstream os;
	os << i;
	return os.str();
}

string int2string_v2(int i)
{
	ostrstream os;
	os << i;
	return os.str();
}

int main()
{
	cout << int2string_v1(10) << endl;
	cout << int2string_v2(10) << endl;

	return 0;
}

上面的这段代码分别使用sstream的string流和strstream流实现了同一个功能:将整数转为string。但是,程序运行后,我们会发现v1版本工作正常,但v2版本有些问题。这是因为sstream的str成员函数返回string,而strstream的str成员函数返回char*,但是返回的这个char*并没有包含字符串结束符’’,这就是为什么说strstream类型不安全。刚才我们介绍过ends,它会给缓冲后面加一个null,所以如果你一定要使用strstream,那ends对你就非常重要了。比如将上面的int2string_v2版本里os << i;改为os << i << ends;就可以正常工作了。但考虑到各方面因素,还是不推荐使用strstream