|
在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 << &#34;...&#34;;
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&#39;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 类型不匹配 &#39;a&#39;就会被设置。如果 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都会终止循环条件。
<hr/>
由于流在使用之前,会查看自己的条件状态是不是 goodbit,如果不是 goodbit 的话,就不会使用这个流。因此,假如三个状态被设置了一个,这个流都不能使用。
while(std::cin>>x>>y) {
std::cout << x << &#39; &#39; << y << &#39;\n&#39;;
}
std::cout << x << &#39; &#39; << y << &#39;\n&#39;;
/*
如上,输入
4 4
a 5
将会输出
4 4
0 4
*/
上面的例子说明,一旦流发生了错误,后面一个 << 操作进入流时,首先检查当前流的状态,如果发现不是 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 << &#34;please enter a value: &#34;;
以写操作为例,例如上面这个输出操作后,他立即就写到对应的文件了吗?其实不然,一般缓冲区在你不告诉他要立即写到对应的文件时,他就很不老实。你做的每次写操作都会先写到它的缓冲区上,等到缓冲区满了,他才会把缓冲区的内容更新到对应的文件上。
这是系统做的一个优化,因为每次流操作都要更新缓冲区的话,IO 代价会特别大。但是一些情况下,你可能特别希望没有缓冲区存在,让数据立即更新到文件上。譬如说:日志文件通过是要实时更新的。
还有一种情况就是,执行到一半的时候,程序崩溃了,那么此时缓冲区的内容...并没有更新到磁盘的文件上。如果你没有实时刷新缓冲区,不断执行这个会崩溃的程序时,由于缓冲区的存在,每次打印出来的内容可能都是不一样的。这对于程序员来说,很难找到程序崩溃的原因,不利于 debugging。
<hr/>
明白了为什么要刷新缓冲区,下面怎么刷新缓冲区、什么时候刷新缓冲区?(注意这里的缓冲区指的是写缓冲区 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 << &#34;hi!&#34; << endl; // writes hi and adds a newline, then flushes the buffer
cout << &#34;hi!&#34; << flush; // writes hi, then flushes the buffer; adds no data
cout << &#34;hi!&#34; << 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 + &#34;.copy&#34;); // 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(&#34;file1&#34;); // out and trunc are implicit
ofstream out2(&#34;file1&#34;, ofstream::out); // trunc is implicit
ofstream out3(&#34;file1&#34;, ofstream::out | ofstream::trunc);
// to preserve the file&#39;s contents, we must explicitly specify app mode
ofstream app(&#34;file2&#34;, ofstream::app); // out is implicit
ofstream app2(&#34;file2&#34;, ofstream::out | ofstream::app);
2.2.2 File Mode Is Determined Each Time open Is Called
对于一个流,每次打开文件时,都可以改变文件模式。
ofstream out; // no file mode is set
out.open(&#34;scratchpad&#34;); // mode implicitly out and trunc
out.close(); // close out so we can use it for a different file
out.open(&#34;precious&#34;, ofstream::app); // mode is out and app
out.close();
3. string Streams

在内存中,可以存在一个 string,然后拥有 stringstream 能够对这个 string 进行流的操作。
- istringstream:从 string 读取数据
- ostringstream:向 string 写入数据
- striingstream:即可从 string 读数据也可向 string 写数据
除了继承自 istream 的操作,stringstream 对象还有几个其它的操作

4. Low-Level IO Operations
4.1 seek & tell
在每个流对象中,维护一个变量表示 当前文件位置,通过这个文件位置对文件读写。如果想要对流中的数据进行随机访问,就需要控制文件位置,修改到一些我们希望的位置开始读写。为此,标准库提供了两个函数:
- seek:将流中的文件位置,定位给给定的函数
- tell:告诉我们当前位置
istream 和 ostream 类型通常不支持随机访问,所以剩下内容只适用于 fstream 和 sstream。例如 cin、cout、cerr 都无法设置随机访问。

对于输出流只能使用p版本,对于输入流只能使用g版本,而对于 fstream、stringstream,两种版本共用一个文件位置,可以混用
4.2 Multi-bytes IO
 |
|