《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()狀態。這樣一個輸入循環結構足夠簡單、足夠夠用,適用不少應用。

相關文章
相關標籤/搜索