C++異常處理

來源:互聯網
上載者:User
引言

異常,讓一個函數可以在發現自己無法處理的錯誤時拋出一個異常,希望它的調用者可以直接或者間接處理這個問題。而傳統錯誤處理技術,檢查到一個局部無法處理的問題時:

1.終止程式(例如atol,atoi,輸入NULL,會產生段錯誤,導致程式異常退出,如果沒有core檔案,找問題的人一定會發瘋)

2.返回一個表示錯誤的值(很多系統函數都是這樣,例如malloc,記憶體不足,分配失敗,返回NULL指標)

3.返回一個合法值,讓程式處於某種非法的狀態(最坑爹的東西,有些第三方庫真會這樣)

4.調用一個預先準備好在出現"錯誤"的情況下用的函數。

第一種情況是不允許的,無條件終止程式的庫無法運用到不能當機的程式裡。第二種情況,比較常用,但是有時不合適,例如返回錯誤碼是int,每個調用都要檢查錯誤值,極不方便,也容易讓程式規模加倍(但是要精確控制邏輯,我覺得這種方式不錯)。第三種情況,很容易誤導調用者,萬一調用者沒有去檢查全域變數errno或者通過其他方式檢查錯誤,那是一個災難,而且這種方式在並發的情況下不能很好工作。至於第四種情況,本人覺得比較少用,而且回調的代碼不該多出現。

使用異常,就把錯誤和處理分開來,由庫函數拋出異常,由調用者捕獲這個異常,調用者就可以知道程式函數庫調用出現錯誤了,並去處理,而是否終止程式就把握在調用者手裡了。

但是,錯誤的處理依然是一件很困難的事情,C++的異常機製為程式員提供了一種處理錯誤的方式,使程式員可以更自然的方式處理錯誤。

異常實戰入門

假設我們寫一個程式,把使用者輸入的兩個字串轉換為整數,相加輸出,一般我們會這麼寫

char *str1 = "1", *str2 = "2";int num1 = atoi(str1);int num2 = atoi(str2);printf("sum is %d\n", num1 + num2);

假設使用者輸入的是str1,str2,如果str1和str2都是整數類型的字串,這段代碼是可以正常工作的,但是使用者的輸入有可能誤操作,輸入了非法字元,例如

char *str1 = "1", *str2 = "a";int num1 = atoi(str1);int num2 = atoi(str2);printf("sum is %d\n", num1 + num2);

這個時候結果是1,因為atoi(str2)返回0。

如果使用者輸入是這樣:

char *str1 = "1", *str2 = NULL;int num1 = atoi(str1);int num2 = atoi(str2);printf("sum is %d\n", num1 + num2);

那麼這段代碼會出現段錯誤,程式異常退出。

atoi我覺得是一個比較危險的函數,如果在一個重要系統中,調用者不知情,傳入了一個NULL字元,程式就異常退出了,導致服務中斷,或者傳入非法字元,結果返回0,代碼繼續走下去,在複雜的系統中想要定位這個問題,真是很不容易。

所以比較合適的方式,是我們用異常處理改造一個安全的atoi方法,叫parseNumber。

class NumberParseException {};bool isNumber(char * str) {     using namespace std;     if (str == NULL)         return false;     int len = strlen(str);     if (len == 0)         return false;     bool isaNumber = false;     char ch;     for (int i = 0; i < len; i++) {         if (i == 0 && (str[i] == '-' || str[i] == '+'))             continue;         if (isdigit(str[i])) {            isaNumber = true;         } else {           isaNumber = false;           break;         }     }     return isaNumber;}int parseNumber(char * str) throw(NumberParseException) {    if (!isNumber(str))        throw NumberParseException();    return atoi(str);}

上述代碼中NumberParseException是自訂的異常類,當我們檢測的時候傳入的str不是一個數字時,就拋出一個數字轉換異常,讓調用者處理錯誤,這比傳入NULL字串,導致段錯誤結束程式好得多,調用者可以捕獲這個異常,決定是否結束程式,也比傳入一個非整數字串,返回0要好,程式出現錯誤,卻繼續無聲無息執行下去。

於是我們之前寫的代碼可以改造如下:

char *str1 = "1", *str2 = NULL;    try {        int num1 = parseNumber(str1);        int num2 = parseNumber(str2);        printf("sum is %d\n", num1 + num2);    } catch (NumberParseException) {        printf("輸入不是整數\n");    }

這段代碼的結果是列印出"輸入不是整數".假設這段代碼是運行在一個遊戲統計系統中,系統需要定時從大量檔案中統計大量使用者進入遊戲頻道1和遊戲頻道2的次數,str1代表進入遊戲頻道1的次數,str2表示進入頻道2的次數,如果不是使用異常,當輸入是NULL程式會導致整個系統宕機,當輸入是非法整數,計算結果全部是錯誤的,當時程式仍然無聲無息"正確執行"。

輸入非法,拋出NumberParseException,即使調用者沒有考慮輸入是非法的,例如是:

 char *str1 = "1", *str2 = "12,";    int num1 = parseNumber(str1);    int num2 = parseNumber(str2);    printf("sum is %d\n", num1 + num2);

就算調用者比較粗心,沒有捕獲異常,程式運行中會拋出NumberParseException,程式宕機,會留下coredump檔案,調用者通過"gdb 程式名 coredump檔案",查看程式宕機時的堆棧,就知道程式運行中,出現了非法整數字元,那麼他就很快知道問題所在,會學乖,把上述代碼改成

char *str1 = "1", *str2 = NULL;    try {        int num1 = parseNumber(str1);        int num2 = parseNumber(str2);        printf("sum is %d\n", num1 + num2);    } catch (NumberParseException) {        printf("輸入不是整數\n");         //列印檔案的路徑,行號,str1,str2等資訊足夠自己去定位問題所在     }

這樣,下次程式出現問題時,調用者就可以定位問題所在了,這就是異常的錯誤處理方式,把錯誤的發現(parseNumber)和錯誤的處理(遊戲統計代碼)分開。

這裡介紹了異常的拋出和捕獲,還有異常的使用情境,接下來就開始一步步講解C++異常。

異常的描述

函數和函數可能拋出的異常集合作為函式宣告的一部分是有價值的,例如

void f(int a) throw (x2,x3);

表示f()只能拋出兩個異常x2,x3,以及這些類型派生的異常,但不會拋出其他異常。如果f函數違反了這個規定,拋出了x2,x3之外的異常,例如x4,那麼當函數f拋出x4異常時,
會轉換為一個std::unexpected()調用,預設是調用std::terminate(),通常是調用abort()。

如果函數不帶異常描述,那麼假定他可能拋出任何異常。例如:

int f();  //可能拋出任何異常

帶任何異常的函數可以用空表表示:

int g() throw (); // 不會拋出任何異常

捕獲異常

捕獲異常的代碼一般如下:

try {    throw E();}catch (H h) {     //何時我們可以能到這裡呢}

1.如果H和E是相同的類型

2.如果H是E的基類

3.如果H和E都是指標類型,而且1或者2對它們所引用的類型成立

4.如果H和E都是參考型別,而且1或者2對H所引用的類型成立

從原則上來說,異常在拋出時被複製,我們最後捕獲的異常只是原始異常的一個副本,所以我們不應該拋出一個不允許拋出一個不允許複製的異常。

此外,我們可以在用於捕獲異常的類型加上const,就像我們可以給函數加上const一樣,限制我們,不能去修改捕捉到的那個異常。

還有,捕獲異常時如果H和E不是參考型別或者指標類型,而且H是E的基類,那麼h對象其實就是H h = E(),最後捕獲的異常對象h會丟失E的附加攜帶資訊。

異常處理的順序
我們之前寫的parseNumber函數會拋出NumberParseException,這個函數只是判斷是否數字才拋出異常,但是沒有考慮,但這個字串表示的整數太大,溢出,拋出異常Overflow.表示如下:

class NumberParseException {};class Overflow : public NumberParseException {};

假設我們parseNumber函數已經為字串的整數溢出做了檢測,遇到這種情況,會拋出Overflow異常,那麼異常捕獲代碼如下:

char *str1 = "1", *str2 = NULL;    try {        int num1 = parseNumber(str1);        int num2 = parseNumber(str2);        printf("sum is %d\n", num1 + num2);    }     catch (Overflow) {        //處理Overflow或者任何由Overflow派生的異常    }    catch (NumberParseException) {         //處理不是Overflow的NumberParseException異常    }

異常組織這種階層對於代碼的健壯性很重要,因為庫函數發布之後,不可能不加入新的異常,就像我們的parseNumber,第一次發布時只是考慮輸入是否一個整數的錯誤,第二次發布時就考慮了判斷輸入的一個字串作為整數是否太大溢出,對於一個函數發布之後不再添加新的異常,幾乎所有的庫函數都不能接受。

如果沒有異常的階層,當函數升級加入新的異常描述時,我們可能都要修改代碼,為每一處調用這個函數的地方加入對應的catch新的異常語句,這很讓你厭煩,程式員也很容易忘記把某個異常加入列表,導致這個異常沒有捕獲,異常退出。

而有了異常的階層,函數升級之後,例如我們的parseNumber加入了Overflow異常描述,函數調用者只需要在自己感興趣的調用情境加入catch(Overflow),並做處理就行了,如果根據不關心Overflow錯誤,甚至不用修改代碼。

未捕獲的異常

如果拋出的異常未被捕捉,那麼就會調用函數std::terminate(),預設情況是調用abort,這對於大部分使用者是正確選擇,特別是排錯程式錯誤的階段(調用abort會產生coredump檔案,coredump檔案的使用可以參考部落格的"學會用core dump偵錯工具錯誤")。

如果我們希望在發生未捕獲異常時,保證清理工作,可以在所有真正需要關注的異常處理之外,再在main添加一個捕捉一切的異常處理,例如:

int main() {    try {        //...     }    catch (std::range_error) {        cerr << "range error\n";     } catch (std::bad_alloc) {        cerr << "new run out of memory\n";     } catch (...) {       //..     }}

這樣就可以捕捉所有的異常,除了那些在全域變數構造和析構的異常(如果要獲得控制,唯一方式是set_unexpected)。
其中catch(...)表示捕捉所有異常,一般會在處理代碼做一些清理工作。

重新拋出

當我們捕獲了一個異常,卻發現無法處理,這種情況下,我們會做完局部能夠做的事情,然後再一次拋出這個異常,讓這個異常在最合適的地方地方處理。例如:

void downloadFileFromServer() {    try {          connect_to_server();          //...     }       catch (NetworkException) {           if (can_handle_it_completely) {               //處理網路異常,例如重連           } else {                throw;            }       }}

這個函數是從遠程伺服器下載檔案,內部調用串連到遠程伺服器的函數,但是可能存在著網路異常,如果多次重連無法成功,就把這個網路異常拋出,讓上層處理。

重新拋出是採用不帶運算對象的throw表示,但是如果重新拋出,又沒有異常可以重新拋出,就會調用terminate();

假設NetworkException有兩個派生異常叫FtpConnectException和HttpConnectException,調用connect_to_server時是拋出HttpConnectException,那麼調用downloadFileFromServer仍然能捕捉到異常HttpConnectException。

標準異常

到了這裡,你已經基本會使用異常了,可是如果你是函數開發人員,並需要把函數給別人使用,在使用異常時,會涉及到自訂異常類,但是C++標準已經定義了一部分標準異常,請儘可能複用這些異常,標準異常參考http://www.cplusplus.com/reference/std/stdexcept/

雖然C++標準異常比較少,但是作為函數開發人員,儘可能還是複用c++標準異常,作為函數調用者就可以少花時間去瞭解的你自訂的異常類,更好的去調用你開發的函數。

總結

本文只是簡單從異常的使用情境,再介紹異常的基本使用方法,一些進階的異常用法沒有羅列,詳細資料可以參考c++之父的C++程式設計語言的異常處理。

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.