《c++程序设计原理与实践》第10章——输入输出流

输入和输出

对于大多数应用,咱们须要某种方法将程序的读写操做与实际进行输入输出的设备分离开。大多数现代操做系统都将I/O设备的处理细节放在设备驱动程序中,经过一个I/O库访问设备驱动程序,这就使不一样设备源的输入输出尽量地类似。若是操做系统能将输入和输出均可以看做字节(字符)流,由输入输出库处理,则程序员地工做就变为:ios

  1. 建立指向恰当数据源和数据目的地I/O流。
  2. 从这些流中读取数据或将数据写入到这些流中。

数据在程序和设备间实际传输的细节都是由I/O库和驱动程序来处理的。git

I/O流模型

C++标准库提供了两种数据类型,istream用于处理输入流,ostream用于处理输出流。
一个ostream能够实现:程序员

  • 将不一样类型的值转换为字符序列。
  • 将这些字符发送到“某处”(如控制台、文件、主存或者另一台计算机)。

一个istream能够实现:函数

  • 将字符序列转换为不一样类型的值。
  • 从某处(如控制台、文件、主存或者另一台计算机)获取字符。

与ostream同样,istream也使用一个缓冲区与操做系统通讯。输出的一个主要目的就是生成可供人们阅读的数据形式,所以,ostream提供了不少特性,用于格式化文本以适应不一样需求。一样,为了易于人们阅读,不少输入数据也是由人们事先编写或者格式化过的,所以,istream提供了一些特性,用于读取由ostream生成的输出内容。工具

文件

一个文件能够简单看做一个从0开始编号的字节序列。每一个文件都有本身的格式,也就是说,有一组规则来肯定其中字节的含义。
对于一个文件,ostream将内存中的对象转换为字节流,再将字节流写到磁盘上。istream进行相反的操做,也就是说,它从磁盘获取字节流,将其转换为对象。
为了读取一个文件,咱们须要:oop

  1. 知道文件名
  2. (以读模式)打开文件。
  3. 读出字符。
  4. 关闭文件(虽然一般文件会被隐式地关闭)。

为了写一个文件,咱们须要:测试

  1. 指定文件名
  2. 按照指定的文件名,(以写模式)打开文件或者建立一个新文件。
  3. 写入咱们的对象。
  4. 关闭文件(虽然一般文件会被隐式地关闭)。

打开文件

若是要读或写一个文件,须要打开一个与文件相关联的流。ifstream是用于读取文件的istream流,ofstream是用于写文件的ostream流,fstream是用于对文件进行读写的iostream流,文件流必须与某个文件相关联,而后才可以使用。例如:编码

//使用头文件#include <fstream>
    cout<<"please enter input file name:"<<endl;
    string iname;
    cin>>iname;
    ifstream ist{iname};
    if(!ist)  error("can't open input file",iname);

用一个名字字符串定义一我的ifstream,能够打开以该字符串为名的文件进行读操做。!ist监测文件是否成功打开。若是成功打开,咱们能够像处理其余任何istream那样从文件中读取数据。例如,假定已经对Point类定义了输入运算符>>,能够写出以下的代码:操作系统

vector<Point>points;
    for(Point p;ist>>p;)
        points.push_back(p);

写文件的过程与读文件相似,经过流ofstream来实现,例如:设计

cout<<"please enter output file name:"<<endl;
    string oname;
    cin>>oname;
    ofstream ost{oname};
    if(!ost)  error("can't open output file",oname);

用一个名字字符串定义一个ofstream,会打开以该字符串为名的文件与流相关联。!ost检测文件是否成功打开。若是打开成功,咱们就能够像处理其余ostream对象同样向文件中写入数据,例如:

for(int p:points)
        ost<<"("<<p.x<<","<<p.y<<endl;

当一个文件流离开了其做用域,与之关联的文件就会被关闭。当文件被关闭时,与之关联的缓冲区会被刷新,也就是说,缓冲区中的字符会被写入到文件中。
通常来讲,最好在程序中一开始的位置,在任何重要的计算都还没有开始以前就打开文件。理想的方式是建立ostream或istream对象时隐式打开文件,并依靠流对象的做用域来关闭文件。例如:

void fill_from_file(vector<Point>&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:"<<endl;
    string iname;
    cin>>iname;
    ifstream ist{iname};
    if(!ist)  error("can't open input file",iname);


    cout<<"please enter output file name:"<<endl;
    string oname;
    cin>>oname;
    ofstream ost{oname};
    if(!ost)  error("can't open output file",oname);

    //典型的输入循环
    vector<Reading>temps;
    int hour;
    double temperature;
    while(ist>>hour>>temperature){
        if(hour<0||23<hour) error("hour out of rang");
        temps.push_back(Reading{hour,temperature});
    }

    for(int i=0;i<temps.size();++i)
        ost<<"("<<temps[i].hour<<","
           <<temps[i].temperature<<endl;
}

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<int>& 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<int>& 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):"<<endl;
    int n=0;
    while(true){
        if(cin>>n){
            if(1<=n&&n<=10) break;
            cout<<"sorry"<<n<<"is not in the [1:10]range;please try again\n";
        }
        else{
            cout<<"sorry,that was not a number;please try again\n";
            skip_to_int();
        }
    }

更好的改进方法是:设计一个读取任意整数的函数,以及一个读取指定范围内整数的函数。

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 "<<low<<" to "<<high<<"(inclusive):"<<endl;
    while(true){
        int n=get_int();
        if(low<=n&&n<=high) return n;
        cout<<"sorry"<<n<<"is not in the ["<<low<<":"<<high<<"]range;please try again\n";
        }
    }
}

将人机对话从函数中分离

在程序中,咱们可能想对用户输出不一样的提示信息,一种可能的实现以下:

int get_int(int low,int high,const string& greeting,const string& sorry)
{
    cout<<greeting<<":["<<low<<":"<<high<<"]\n";
    while(true){
        int n=get_int();
        if(low<=n&&n<=high) return n;
        cout<<sorry<<":["<<low<<":"<<high<<"]\n";
        }
    }
}

“工具函数”会在程序中不少地方被调用,所以不该该将提示信息“硬编码”到函数中。更进一步,库函数会在不少程序中被使用,也不该该向用户输出任何信息。

用户自定义输出运算

ostream& operator<<(ostream& os,const Date& d)
{
    return os<<"("<<d.year()
             <<","<<d.month()
             <<","<<d.day()<<")";
}

这个输出运算符会将2004年8月30日打印为“(2004,8,30)”的形式。假定已经为Date定义了上面的<<操做符,那么
cout<<d1;
其中d1是Date类型的对象,等价于下面的调用:
operator<<(cout,d1);
须要注意operator<<()是如何接受一个ostream&做为第一个参数,又将其做为返回值返回的。这就是为何能够将输入操做“连接”起来的缘由,由于输出流按这种方式逐步传递下去了。例如:

cout<<d1<<d2;           //意味着operato<<(count,d1)<<d2;             
    cout<<d1<<d2;           //意味着operator<<(operator<<(cout,d1),d2);

也就是说,连续输出两个对象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()状态。这样一个输入循环结构足够简单、足够够用,适用不少应用。

相关文章
相关标签/搜索