C++處理輸入輸出錯誤

2020-07-16 10:05:21
當處理輸入輸出時,我們必須預計到其中可能發生的錯誤並給出相應的處理措施。
  • 當我們輸入時,可能會由於人的失誤(錯誤理解了指令、打字錯誤、允許自家的小貓在鍵盤上散步等)、檔案格式不符、錯誤估計了情況等原因造成讀取失敗。
  • 當我們輸出時,如果輸出裝置不可用、佇列滿或者發生了故障等,都會導致寫入失敗。

發生輸入輸出錯誤的可能情況是無限的!但 C++ 將所有可能的情況歸結為四類,稱為流狀態(stream state)。每種流狀態都用一個 iostate 型別的標誌位來表示。

流狀態對應的標誌位
標誌位 意義
badbit 發生了(或許是物理上的)致命性錯誤,流將不能繼續使用。
eofbit 輸入結束(檔案流的物理結束或使用者結束了控制台流輸入,例如使用者按下了 Ctrl+Z 或 Ctrl+D 組合鍵。
failbit I/O 操作失敗,主要原因是非法資料(例如,試圖讀取數位時遇到字母)。流可以繼續使用,但會設定 failbit 標誌。
goodbit 一切止常,沒有錯誤發生,也沒有輸入結束。

ios_base 類定義了以上四個標誌位以及 iostate 型別,但是 ios 類又派生自 ios_base 類,所以可以使用 ios::failbit 代替 ios_base::failbit 以節省輸入。

一旦流發生錯誤,對應的標誌位就會被設定,我們可以通過下表列出的函數檢測流狀態。

C++流狀態檢測函數及其說明
檢測函數 對應的標誌位 說明
good() goodbit 操作成功,沒有發生任何錯誤。
eof() eofbit 到達輸入末尾或檔案尾。
fail() failbit 發生某些意外情況(例如,我們要讀入一個數位,卻讀入了字元 'x')。
bad() badbit 發生嚴重的意外(如磁碟讀故障)。

不幸的是,fail() 和 bad() 之間的區別並未被準確定義,程式設計師對此的觀點各種各樣。但是,基本的思想很簡單:
  • 如果輸入操作遇到一個簡單的格式錯誤,則使流進入 fail() 狀態,也就是假定我們(輸入操作的使用者)可以從錯誤中恢復。
  • 如果錯誤真的非常嚴重,例如發生了磁碟故障,輸入操作會使得流進入 bad() 狀態。也就是假定面對這種情況你所能做的很有限,只能退出輸入。

以上觀點導致如下邏輯:
int i = 0;
cin >> i;
if(!cin){  //只有輸入操作失敗,才會跳轉到這裡
    if(cin.bad()){  //流發生嚴重故障,只能退出函數
        error("cin is bad!");  //error是自定義函數,它丟擲異常,並給出提示資訊
    }
    if(cin.eof()){  //檢測是否讀取結束
        //TODO:
    }
    if(cin.fail()){  //流遇到了一些意外情況
        cin.clear(); //清除/恢復流狀態
        //TODO:
    }
}
!cin 可以理解為“cin 不成功”或者“cin 發生了某些錯誤”或者“ cin 的狀態不是 good()”, 這與“操作成功”正好相反,《C++ cin判斷輸入結束》一節中對此有詳解。

請注意我們在處理 fail() 時所使用的 cin.clear()。當流發生錯誤時,我們可以進行錯誤恢復。為了恢復錯誤,我們顯式地將流從 fail() 狀態轉移到其他狀態,從而可以繼續從中讀取字元。clear() 就起到這樣的作用——執行 cin.clear() 後,cin 的狀態就變為 good()。

範例

下面是一個如何使用流狀態的例子。假定我們要讀取一個整數序列並存入 vector 中,字元*或“檔案尾”表示序列結束。Windows 平台按下 Ctrl+Z 組合鍵,再按下確認鍵表示到達檔案末尾;類Unix系統按下 Ctrl+D 組合鍵表示到達檔案末尾。

上述功能可通過如下函數來實現:
//從 ist 中讀入整數到 v 中,直到遇到 eof() 或終結符
void fill_vector(istream& ist, vector<int>& v, char terminator){
    for( int i; ist>>i; ) v.push_back(i);

    //正常情況
    if(ist.eof()) return;  //發現到了檔案尾,正確,返回

    //發生嚴重錯誤,只能退出函數
    if (ist.bad()){
        error("cin is bad!");  //error是自定義函數,它丟擲異常,並給出提示資訊
    }

    //發生意外情況
    if (ist.fail()) {  //最好清除混亂,然後匯報問題
        ist.clear();  //清除流狀態

        //檢測下一個字元是否是終結符
        char c;
        ist>>c;  //讀入一個符號,希望是終結符
        if(c != terminator) { // 非終結符
            ist.unget(); //放回該符號
            ist.clear(ios_base::failbit);  //將流狀態設定為 fail()
        }
    }
}
如果發生了 fail(),我們嘗試檢測下一個字元是否是結束符:如果是,那麼就完整得讀取了資料,使用 clear() 恢復狀態就可以;如果不是,我們就沒有辦法處理了,所以將狀態重新設定為 fail(),以期望 fill_vector() 的呼叫者(上層函數)有能力處理。

我們通過呼叫 ist.clear(ios_base::failbit) 來將流狀態設定為 fail()。對照簡單的cleal(),帶引數的用法有些令人迷惑:當 clear() 帶引數時,引數中所指出的 iostream 狀態位會被置位(進入相應狀態),而未指出的狀態位會被復位。通過將流狀態設定為 fail(),我們表明遇到了一個格式錯誤,而不是一個更為嚴重的問題。

可以用 unget() 將字元放回 ist,以便 fill_vector() 的呼叫者可能使用該字元。unget() 函數是 putback() 的簡化版本,它依賴於流物件記住最後一個字元是什麼,所以在這裡可以不用考慮它的用法。

如果 fill_vector() 的呼叫者想知道是什麼原因終止了輸入,那麼可以檢測流是處於 fail() 還是 eof() 狀態。當然也可以捕獲 error() 丟擲的 runtime_error 異常,但當 istream 處於 bad() 狀態時,繼續獲取資料是不可能的。大多數的呼叫者無須為此煩惱。因為這意味著,幾乎在所有情況下,對於 bad() 狀態,我們所能做的只是丟擲一個異常。

簡單起見,可以讓 istream 幫我們丟擲這個異常。

//當 ist 出現問題時拋出異常
ist.exceptions(ist.exceptions() | ios_base:: badbit);

這樣的寫法也許看起來有些奇怪,但結果卻很簡單,當此語句執行時,如果 ist 處於 bad() 狀態,它會丟擲一個標準庫異常 ios_base::failure。在一個程式中,我們只需要呼叫 exceptions() 一次。這允許我們簡化關聯於 ist 的所有輸入過程,同時忽略對 bad() 的處理:
//從ist中讀入整數到v中,直到遇到eof()或終結符
void fill_vector(istream& ist, vector<int>& v, char terminator){
    ist.exceptions(ist.exceptions() | ios_base:: badbit);

    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); //將流狀態設定為 fail()
    }
}
這裡使用了 ios_base,它是 iostream 的一部分,包含了對常數如 badbit 的定義、異常如 failure 的定義,以及其他一些有用的定義。可以通過::操作符來使用它們,例如 ios_ base::badbit。

我們無須如此深入地討論 iostream 庫的細節,若要學習 iostream的所有內容,可能需要一門完整的課程。例如,iostream 可以處理不同的字元集,實現不同的緩衝策略,還包含一些工具,能按不同語言的習慣格式化貨幣金額的輸入輸出。我們曾經收到過一份關於烏克蘭貨幣輸入輸出格式的錯誤報告。如果需要了解更多 iostream 庫的內容,可以參考 Stroustrup 的《The C++ Programming Language》和 Langer 的《Standard C++ IOStreams and Locales》。

與 istream—樣,ostream 也有四個狀態:good()、fail()、eof() 和 bad()。不過,對於本教學的讀者來說,輸出錯誤要比輸入錯誤少得多,因此通常不對 ostream 進行狀態檢測。如果程式執行環境中輸出裝置不可用、佇列滿或者發生故障的概率很高,我們就可以像處理輸入操作那樣,在每次輸出操作之後都檢測其狀態。