在本章和下一章中,我们将介绍C++标准库中用于处理来自各种源的输入和输出的功能:I/O流。本章关注基本模型:如何读写单个值,以及如何打开和读写整个文件。下一章将介绍具体细节。
10.1 输入和输出
如果没有数据,计算就毫无意义。我们需要将数据输入到程序中来进行一些有价值的计算,并获取输出。数据的输入源和输出目标非常广泛。因此,我们需要一种将程序的读写操作与实际使用的输入/输出设备分离的方法。
I/O库提供了I/O的一个抽象,从而程序员不必关心设备和设备驱动程序:
使用这样是模型,输入和输出就可以看作由I/O库处理的字节(字符)流。程序员的工作就变为:
- 创建数据源或目的地的I/O流
- 读写这些流
从程序员的角度,输入和输出有很多种。例如:
- 大量数据项构成的流(文件、网络连接、录音设备、显示设备等)
- 通过键盘与用户交互
- 通过图形界面与用户交互
其中,前两种I/O由C++标准库I/O流提供,图形用户交互则由其他一些库支持(第12~16章)。
10.2 I/O流模型
C++标准库<iostream>提供了istream
类型来处理输入流、ostream
类型来处理输出流。我们已经使用过标准输入流cin
和标准输出流cout
。
注:各种I/O流及继承关系如下图所示(详见Input/Output library)
ostream
负责
- 将不同类型的值转换为字符序列
- 将这些字符发送到“某处”(例如控制台、文件、内存或者另一台计算机)
缓冲区(buffer)是ostream
内部用于保存数据并与操作系统通信的数据结构。如果写入ostream
和字符出现在目的设备之间存在“延迟”,通常是因为字符还在缓冲区中。缓冲是提高性能的重要技术,而处理大量数据时性能是很重要的。
istream
负责
- 将字符序列转换为不同类型的值
- 从“某处”(例如控制台、文件、内存或者另一台计算机)读取字符
与ostream
一样,istream
也使用缓冲区来与操作系统通信。对于istream
,缓冲区对用户是可见的。例如,使用cin
从键盘输入数据时,输入的内容都留在缓冲区中,直到按Enter键,在此之前可以通退格键清除字符来“改变主意”。
10.3 文件
基本上,文件就是一个从0开始编号的字节序列:
文件具有格式,即有一组规则来确定字节的含义。 例如,文本文件中,一个字节表示一个字符(在特定字符集下的编码);使用二进制表示整数的文件中,4个字节表示一个整数,如下图所示。只有知道文件的格式才能知道文件中字节数据的含义。
例如,在文本文件中,48 65 6c 70四个字节分别表示 ‘H’, ‘e’, ‘l’, ‘p’ 四个字符;而在二进制整数文件中,同样的四个字节表示十六进制数0x48656c70,等于十进制的1214606448。
注:一个字节(byte)是8个比特位(bit),能表示的二进制数范围是00000000~11111111,即十进制的0~255、十六进制的00~ff。
对于一个文件,ostream
将内存中的对象转换为字节流,并将其写入磁盘。istream
进行相反的操作:从磁盘读取字节流,并将其转换为对象:
为了读一个文件,需要知道文件名,(以读模式)打开文件,读入字符,关闭文件(通常隐式完成)。为了写一个文件,需要知道文件名,(以写模式)打开文件,写出对象,关闭文件(通常隐式完成)。
10.4 打开文件
如果要读写文件,必须打开一个专门用于该文件的流。ifstream
是用于读文件的istream
,ofstream
是用于写文件的ostream
,fstream
是既可以读文件又可以写文件的iostream
,这些类型定义在标准库头文件 <fstream> 中。在使用文件流之前,必须将其关联到文件。例如:
// write to file
string filename = "test.txt";
ofstream ofs(filename); // open file for writing
if (!ofs.is_open()) // check if file is open
cout << "failed to open " << filename << endl;
else
ofs << 123 << "abc"; // write to ofstream
ofstream
的构造函数参数指定文件名,如果文件不存在则创建。成员函数is_open()
检查文件是否被成功打开(文件流是否成功关联到文件),如果打开失败则返回false
。之后可以像任何ostream
一样使用运算符<<
写出数据。
例如,如果上面的代码运行成功,则文件test.txt的内容如下:
123abc
ifstream
的用法与ofstream
类似:
// read from file
string filename = "test.txt";
ifstream ifs(filename); // open file for reading
if (!ifs.is_open()) // check if file is open
cout << "failed to open " << filename << endl;
else {
int n;
string s;
ifs >> n >> s; // read from ifstream
cout << "n = " << n << ", s = " << s << endl;
}
ifstream
的构造函数参数指定文件名,文件必须存在。成员函数is_open()
检查文件是否被成功打开,如果因文件不存在、没有权限等原因打开失败则返回false
。之后可以像任何istream
一样使用运算符>>
读取数据。
例如,text.txt的内容为上一段代码的输出结果,则这段代码的输出如下:
n = 123, s = abc
通常,最好在重要的计算开始之前就打开文件。毕竟,如果在完成计算之后才发现无法保存结果将会浪费计算资源。
当一个文件流离开作用域时,它关联的文件将被关闭,文件流内部的缓冲区会被刷新(flush),即缓冲区中的字符会被写入文件。
在创建文件流时打开文件、依赖流的作用域来隐式关闭文件是一种理想的方法。 另外,也可以通过open()
和close()
函数显式打开和关闭文件。然而,依赖作用域的方式避免了两类错误:在打开文件之间或关闭文件之后使用文件流对象。例如:
ifstream ifs;
// ...
ifs >> foo; // won't succeed: no file opened for ifs
// ...
ifs.open(name); // open file named name for reading
// ...
ifs.close(); // close file
// ...
ifs >> bar; // won't succeed: ifs's file was closed
// ...
不能在关闭一个文件流之前第二次打开它。例如:
fstream fs;
fs.open("foo", ios_base::in); // open for input
// close() missing
fs.open("foo", ios_base::out); // won't succeed: fs is already open
if (!fs) error("impossible");
在打开一个流之后不要忘记检测是否成功。
注:
(1)fstream
构造函数的第二个参数是打开模式,其类型是枚举ios_base::openmode,其各枚举值都是只有一个二进制位为1、其他位均为0的整数(2的幂),即位掩码,可以使用按位或运算符|
组合。C++文件流的打开模式与C标准库fopen()
函数的打开模式对应关系如下:
C++模式 | C模式 | 含义 |
---|---|---|
in | r | 读 |
out 或 out | trunc | w | 写(覆盖原有内容) |
app 或 out | app | a | 追加 |
in | out | r+ | 读或写 |
in | out | trunc | w+ | 读或写(覆盖原有内容) |
in | out | app | a+ | 读或追加 |
in | binary | rb | 读二进制文件 |
out | binary 或 out | trunc | binary | wb | 写二进制文件(覆盖原有内容) |
app | binary 或 out | app | binary | ab | 追加二进制文件 |
in | out | binary | r+b | 读或写二进制文件 |
in | out | trunc | binary | w+b | 读或写二进制文件(覆盖原有内容) |
in | out | app | binary | a+b | 读或追加二进制文件 |
另外,ifstream
和ofstream
的构造函数也有第二个参数,默认值分别是ios_base::in
和ios_base::out
,即使指定了其他模式,也会分别自动添加ios_base::in
和ios_base::out
。
(2)在上面的代码中,第二次调用open()
后,fs
通过运算符operator bool()
转换为布尔值false
,表示处于失败状态(详见10.6节),但is_open()
仍然为true
;如果第一次open()
失败了,则fs
和is_open()
都是false
。
10.5 读写文件
考虑这样一个问题:如何从文件读取一组测量结果并在内存中表示?例如,从气象站获取的温度数据:
0 60.7
1 60.6
2 60.3
3 59.22
...
这个数据文件包含了一系列(小时,温度)数值对。小时的值为0~23,温度为华氏度,没有任何其他格式。这是最简单的情况。
读取温度数据文件
其中的while
循环是一个典型的输入循环,ifs
可以是任何一种istream
,这段代码都能够适用,因为所有的istream
都支持运算符>>
。对于输出流也是同理。
10.6 I/O错误处理
在处理输入时,我们必须预料到并处理错误。 错误的原因可能是人为失误(理解错了指令、打字错误、让猫在键盘上散步等)、文件格式不符合规范、我们(程序员)预料错误,等等。输入错误的可能性是无限的,但istream
将所有可能的情况归结为四类,称为流状态(stream state)。流状态用枚举ios_base::iostate表示,也是位掩码:
流状态 | 含义 |
---|---|
goodbit | 操作成功 |
eofbit | 到达输入结尾(“end of file”, EOF) |
failbit | 发生意外情况 |
badbit | 发生严重意外情况 |
注:istream
提供了一些成员函数来检查和更新错误状态
成员函数 | 作用 |
---|---|
rdstate() | 返回流状态 |
setstate() | 将流状态与给定值按位取或(即设置指定的状态位,其他状态位不变) |
clear() | 将流状态设置为给定值,默认为goodbit (即设置指定的状态位,清除其他状态位) |
good() | 如果没有任何错误标识被置位则返回true ,等价于rdstate() == goodbit |
eof() | 如果eofbit 被置位则返回true |
fail() | 如果failbit 或badbit 被置位则返回true |
bad() | 如果badbit 被置位则返回true |
operator bool | 等价于!fail() |
operator! | 等价于fail() |
failbit
和badbit
之间的区别并未准确定义(由流的作者决定),但基本思想是:如果输入操作遇到简单的格式错误(例如读取数字时遇到 ‘x’)则设置failbit
,即假定此时可以从错误中恢复;如果遇到严重错误(例如磁盘读故障)则设置badbit
,即假定此时只能放弃从这个流获取数据。bad()
状态的流也是fail()
状态,因此有如下的通用逻辑:
int i = 0;
s >> i;
if (!s) {
// we get here (only) if an input operation failed
if (s.bad()) // stream corrupted: let's get out of here!
error("cin is bad");
if (s.eof()) {
// no more input
// this is often how we want a sequence of input operations to end
}
if (s.fail()) {
// stream encountered something unexpected
s.clear(); // make ready for more input
// somehow recover
}
}
注意处理fail()
时所使用的clear()
:为了从错误中恢复,通过clear()
将流恢复到good()
状态(否则后续输入操作都会失败)。
下面是一个如何使用流状态的例子。假设要读取一系列整数到一个vector
中,以字符 “*” 或EOF(Windows系统是Ctrl+Z,UNIX系统是Ctrl+D)结束。例如:
1 2 3 4 5 *
可以使用以下函数实现:
读取整数向量
这段代码看似不复杂,但实际上有很多需要注意的细节:
- 运算符
>>
遇到输入结尾时,输入流的eofbit
和failbit
都会被设置。运算符>>
的返回值是输入流本身,而istream
定义了operator bool
(即将输入流本身解释为布尔值),因此将ist >> i
作为for
循环测试条件的含义是:先执行输入操作,之后判断ist.operator bool()
是否为true
。另外,operator bool
等价于!fail()
,而fail()
在failbit
或badbit
被设置时都会返回true
。综上,无论输入操作遇到输入结尾、格式错误还是严重错误,fail()
都会返回true
,operator bool
都会返回false
。 - 在尝试读取终结符之前,必须先调用
clear()
将所有的错误状态清除,否则输入操作ist >> c
将会失败。 - 如果读取的字符不是终结符,则使用
unget()
将该字符放回ist
(输入操作不能随意丢弃未使用的字符),并通过ist.clear(ios_base::failbit)
将流状态重新设置为failbit
(表示遇到了格式错误)。带参数的clear()
有些令人迷惑:设置指定的状态位,清除其他状态位。unget()
是putback()
(见6.8.1节)的简洁版,它依赖流记住最后一个字符是什么,而不需要在参数中给出。 exceptions()
是作用是:当流处于指定的状态时,将抛出标准库异常ios_base::failure
。因此在fill_vector()
中不必单独处理bad()
(在几乎所有情况下,我们能做的也只是抛出异常)。- 如果需要,
fill_vector()
的调用者可以通过测试ist
的eof()
、fail()
或捕获异常来知道输入终止的原因。
ostream
也有与istream
相同的四种状态。但对于本书中的程序,输出错误比输入错误罕见得多,因此通常不检测ostream
的状态。