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);