输入和输出
对于大多数应用,我们需要某种方法将程序的读写操作与实际进行输入输出的设备分离开。大多数现代操作系统都将I/O设备的处理细节放在设备驱动程序中,通过一个I/O库访问设备驱动程序,这就使不同设备源的输入输出尽可能地相似。如果操作系统能将输入和输出都可以看作字节(字符)流,由输入输出库处理,则程序员地工作就变为:
- 创建指向恰当数据源和数据目的地I/O流。
- 从这些流中读取数据或将数据写入到这些流中。
数据在程序和设备间实际传输的细节都是由I/O库和驱动程序来处理的。
I/O流模型
C++标准库提供了两种数据类型,istream用于处理输入流,ostream用于处理输出流。
一个ostream可以实现:
- 将不同类型的值转换为字符序列。
- 将这些字符发送到“某处”(如控制台、文件、主存或者另外一台计算机)。
一个istream可以实现:
- 将字符序列转换为不同类型的值。
- 从某处(如控制台、文件、主存或者另外一台计算机)获取字符。
与ostream一样,istream也使用一个缓冲区与操作系统通信。输出的一个主要目的就是生成可供人们阅读的数据形式,因此,ostream提供了很多特性,用于格式化文本以适应不同需求。同样,为了易于人们阅读,很多输入数据也是由人们事先编写或者格式化过的,因此,istream提供了一些特性,用于读取由ostream生成的输出内容。
文件
一个文件可以简单看作一个从0开始编号的字节序列。每个文件都有自己的格式,也就是说,有一组规则来确定其中字节的含义。
对于一个文件,ostream将内存中的对象转换为字节流,再将字节流写到磁盘上。istream进行相反的操作,也就是说,它从磁盘获取字节流,将其转换为对象。
为了读取一个文件,我们需要:
- 知道文件名
- (以读模式)打开文件。
- 读出字符。
- 关闭文件(虽然通常文件会被隐式地关闭)。
为了写一个文件,我们需要:
- 指定文件名
- 按照指定的文件名,(以写模式)打开文件或者创建一个新文件。
- 写入我们的对象。
- 关闭文件(虽然通常文件会被隐式地关闭)。
打开文件
如果要读或写一个文件,需要打开一个与文件相关联的流。ifstream是用于读取文件的istream流,ofstream是用于写文件的ostream流,fstream是用于对文件进行读写的iostream流,文件流必须与某个文件相关联,然后才可使用。例如:
//使用头文件#include
cout<<"please enter input file name:"<>iname;
ifstream ist{iname};
if(!ist) error("can't open input file",iname);
用一个名字字符串定义一个人ifstream,可以打开以该字符串为名的文件进行读操作。!ist监测文件是否成功打开。如果成功打开,我们可以像处理其他任何istream那样从文件中读取数据。例如,假定已经对Point类定义了输入运算符>>,可以写出如下的代码:
vectorpoints;
for(Point p;ist>>p;)
points.push_back(p);
写文件的过程与读文件类似,通过流ofstream来实现,例如:
cout<<"please enter output file name:"<>oname;
ofstream ost{oname};
if(!ost) error("can't open output file",oname);
用一个名字字符串定义一个ofstream,会打开以该字符串为名的文件与流相关联。!ost检测文件是否成功打开。如果打开成功,我们就可以像处理其他ostream对象一样向文件中写入数据,例如:
for(int p:points)
ost<<"("<
当一个文件流离开了其作用域,与之关联的文件就会被关闭。当文件被关闭时,与之关联的缓冲区会被刷新,也就是说,缓冲区中的字符会被写入到文件中。
一般来说,最好在程序中一开始的位置,在任何重要的计算都尚未开始之前就打开文件。理想的方式是创建ostream或istream对象时隐式打开文件,并依靠流对象的作用域来关闭文件。例如:
void fill_from_file(vector&points,string& name)
{
ifstream ist{name};
if(!ist) error("can't open input file",iname);
//使用ist
//在退出函数时文件被隐式关闭
}
此外,还可以通过open()和close()操作显式打开和关闭文件。但是,依靠作用域的方式最大程度地降低了两类错误出现的概率:在打开文件之前或关闭之后使用文件流对象。例如:
ifstream ifs;
//...
ifs>>foo; //不会成功:没有为ifs打开的文件
//...
ifs.open(name,ios_base::in); //打开以name命名的文件进行读操作
//...
//ifs.open(name,ios_base::out); //打开以name命名的文件进行写操作
//...
ifs.close(); //关闭文件
//...
ifs>>bar; //不会成功:ifs对应的文件已经关闭
//...
在真实的程序中,通常这类错误更难以定位。幸运的是,我们不能在还没有关闭一个文件时就第二次打开它。因此,在打开一个文件之后,一定不要忘记检测流对象是否成功关联了。在使用文件的范围不能简单包含于任何流对象的作用域中,就需要显式使用open()和close()操作,不过这种情况非常少见。
读写文件
假如一个数据文件由一个(小时,温度)数值对序列组成,如下所示:
0 60.7
1 60.6
2 60.3
3 59.22
...
这部分讨论文件中不包含任何特殊的头信息(例如温度读数是从哪里获得的)、值的单位、标点(例如为每对数值加上括号)或者终止符。这是一个最为简单的情形。
#include "std_lib_facilities.h"
struct Reading{ //温度数据读取
int hour; //在[0:23]区间取值的小时数
double temperature; //华氏温度
};
int main()
{
cout<<"please enter input file name:"<>iname;
ifstream ist{iname};
if(!ist) error("can't open input file",iname);
cout<<"please enter output file name:"<>oname;
ofstream ost{oname};
if(!ost) error("can't open output file",oname);
//典型的输入循环
vectortemps;
int hour;
double temperature;
while(ist>>hour>>temperature){
if(hour<0||23
istream流ist可以是一个输入文件流(ifstream),也可以是标准输入流cin(的一个别名),或者是任何其他类型的istream。对于这段代码而言,它并不关心这个istream是从哪里获取数据。我们的程序所关心的只是:ist是一个istream,而且数据格式如我们所期望)。
写文件通常比读文件简单。再重复一遍,一旦一个流对象已经被初始化,我们就可以不必了解它到底是哪种类型的流。
I/O错误处理
istream将所有可能的情况归结为四类,称为流状态
good() //操作成功
eof() //到达输入末尾(“文件尾”)
fail() //发生某些意外情况(例如,我们要读入一个数字,却读入了字符‘x’)
bad() //发生严重的意外(如磁盘读故障)
如果输入操作遇到一个简单的格式错误,则使流进入fali()状态,也就是假定我们(输入操作的用户)可以从错误中恢复。另一方面,如果错误真的非常严重,例如发生了磁盘读故障,输入操作会使得流进入bad()状态,也就是假定面对这种情况能做的很有限,只能退出输入。这种观点导致以下逻辑:
int i=0;
cin>>i;
if(!cin){ //只有输入操作失败,才会跳转到这里
if(cin.bad()) error("cin is bad"); //流发生故障:让我们跳出程序!
if(cin.eof()){
//没有任何输入
//这是我们结束程序经常需要的输入操作序列
}
if(cin.fail()){ //流遇到了一些意外情况
cin.clear(); //为更多的输入操作做准备
//恢复流的其他操作
}
}
当流发生错误时,我们可以进行错误恢复。为了恢复错误,我们显式地将流从fail()状态转移到其他状态,从而可以继续从中读取字符。clear()就起到这样的作用——执行cin.clear()后,cin的状态就变成good()。
例如:
1 2 3 4 5 *
可以通过如下函数实现
void fill_vector(istream& ist,vector& v,char terminator)
//从ist中读入整数列到v中,直到遇到eof()或终结符 1.0
{
for(int i;ist>>i;) v.push_back(i);
if(ist.eof()) return; //发现到了文件尾
if(ist.bad()) error("ist is bad"); //流发生故障:让我们跳出程序!
if(ist.fail()){ //最好清楚混乱,然后汇报问题
ist.clear(); //清除流状态
char c;
ist>>c; //读入一个符号,希望是终结符
if(c!=terminator){ //非终结符
ist.unget(); //放回该符号
ist.clear(ios_base::failbit); //将流状态设置为fail()
}
}
}
注意,即使没有遇到终结符,函数也会返回。毕竟,我们可能已经读取了一些数据,而fill_vector()的调用者也许有能力从fail()状态中恢复过来。由于我们已经清除了状态(clear()函数)以便检查后续字符,所以必须将流状态重新置为fail()。我们通过调用ist.clear(ios_base::failbit)来达到这一目的。当clear()调用带参数时,参数中所指出的iostream状态位会被置位(进入相应状态),而未指出的状态位会被复位。可以用unget()将字符放回ist,以便fill_vector()的调用者可能使用该字符,unget()依赖于流对象记住最后一个字符是什么。
对于bad()状态,我们所能做的只是抛出一个异常。简单起见,可以让istream帮我们来做。
//当ist出现问题时抛出异常
ist.exceptions(ist.exception()|ios_base::badbit);
当此语句执行时,如果ist处于bad()状态,它会抛出一个标准库异常ios_base::failure。在一个程序中,我们只需要调用exception()一次。这简化了关联于ist的所有输入过程,同时忽略对bad()的处理:
void fill_vector(istream& ist,vector& v,char terminator)
//从ist中读入整数列到v中,直到遇到eof()或终结符 优
{
for(int i;ist>>i;) v.push_back(i);
if(ist.eof()) return;
//不是good(),不是bad(),不是eof(),ist的状态一定是fail()
ist.clear();
char c;
ist>>c;
if(c!=terminator){
ist.unget();
ist.clear(ios_base::failbit);
}
}
与istream一样,ostream也有四个状态:good()、fail()、eof()和bad()。如果程序运行环境中输出设备不可用、队列满或者发生故障的概率很高,我们就可以像处理输入操作那样,在每次输出操作之后都检测其状态。
读取单个值
现在我们已经知道如何读取以文件尾或者某个特定终结符结束的值序列了。接下来考虑一个十分常见的应用问题:不断要求用户输入一个值,直到用户输入的值合乎要求为止。假定我们要求用户输入1到10之间的整数。
将程序分解为易管理的子模块
一种常用的令代码更为清晰的方法是将逻辑上做不同事情的代码划分为独立的函数。例如,对于发现“问题字符”(如意料之外的字符)后进行错误恢复的代码,就可以将其分离出来:
void skip_to_int()
{
if(cin.fail()){ //我们发现了非整型的符号
cin.clear(); //我们想要查看这些符号
for(char ch;cin>>ch;){ //忽略非数值符号
if(isdigit(ch)||ch=='-'){
cin.unget(); //将数字放回
return;
}
}
}
error("no input"); //eof或者bad状态:放弃
}
有了以上的“工具函数”skip_to_int()后,代码就可以改写为:
//1.0
cout<<"please enter an integer in the range 1 to 10(inclusive):"<>n){
if(1<=n&&n<=10) break;
cout<<"sorry"<
更好的改进方法是:设计一个读取任意整数的函数,以及一个读取指定范围内整数的函数。
int get_int()
{
int n=0;
while(true){
if(cin>>n) return n;
cout<<"sorry,that was not a number;please try again\n";
skip_to_int();
}
}
int get_int(int low,int high)
{
cout<<"please enter an integer in the range "<
将人机对话从函数中分离
在程序中,我们可能想对用户输出不同的提示信息,一种可能的实现如下:
int get_int(int low,int high,const string& greeting,const string& sorry)
{
cout<
“工具函数”会在程序中很多地方被调用,因此不应该将提示信息“硬编码”到函数中。更进一步,库函数会在很多程序中被使用,也不应该向用户输出任何信息。
用户自定义输出运算
ostream& operator<<(ostream& os,const Date& d)
{
return os<<"("<
这个输出运算符会将2004年8月30日打印为“(2004,8,30)”的形式。假定已经为Date定义了上面的<<操作符,那么cout<
其中d1是Date类型的对象,等价于下面的调用:operator<<(cout,d1);
需要注意operator<<()是如何接受一个ostream&作为第一个参数,又将其作为返回值返回的。这就是为什么可以将输入操作“链接”起来的原因,因为输出流按这种方式逐步传递下去了。例如:
cout<
也就是说,连续输出两个对象d1和d2,d1的输出流是cout,而d2的输出流是第一个输出操作的返回结果。
用户自定义输入运算符
istream& operator>>(istream& is,Date& dd)
{
int y,m,d;
char ch1,ch2,ch3,ch4;
is>>ch1>>y>>ch2>>m>>ch3>>d>>ch4;
if(!is) return is;
if(ch1!='('||ch2!=','||ch3!=','||ch4!=')'){
is.clear(ios_base::failbit);
return is;
}
dd=Date{y,Date::Month(m),d};
return is;
}
对于一个operator>>()来说,理想目标是不读取或丢弃任何它未用到的字符,但这太困难了:因为在捕获到一个格式错误之前就已经读入了大量字符,唯一肯定可以保证的是用unget()退回一个字符。
一个标准的输入循环
下面给出了一个通用的解决策略,假定ist是一个输入流:
for(My_type var;ist>>var;){ //一直读到文件结束
//或许会检查var的有效性
//并用var来执行什么操作
}
if(ist.bad()) error("bad input stream");
if(ist.fail()){
//这是一个可接受的终结符吗?
}
//继续:我们发现了文件尾
也就是说,我们读入一组值,将其保存到变量中,当无法再读入更多值的时候,需要检查流的状态,看是什么原因造成的。我们可以向前面介绍的那样,让输入流在发生错误时抛出一个failure异常,以免我们需要不断检查发生的故障。
//在某处:使ist在处于bad状态时抛出一个异常
ist.exceptions(ist.exception()|ios_base::badbit);
我们也可以指定一个字符作为终结符,用一个函数实现检测
//在某处:使ist在处于bad状态时抛出一个异常
ist.exceptions(ist.exception()|ios_base::badbit);
void end_of_loop(istream& ist,char term,const string& message)
{
if(ist.fail()){
ist.clear();
char ch;
if(ist>>ch&&ch==term) return; //所有的都正常
error(message);
}
}
于是输入循环变为:
for(My_type var;ist>>var;){ //一直读到文件结束
//或许会检查var的有效性
//并用var来执行什么操作
}
end_of_loop(ist,"|","bad termination of file"); //测试我们是否可以继续
函数end_of_loop()什么也不做,除非流处于fail()状态。这样一个输入循环结构足够简单、足够够用,适用很多应用。