1. 程式人生 > >C++工具--異常及異常處理

C++工具--異常及異常處理

我們總是希望自己所寫的程式都是正確,並且執行結果也是正確的。但這是很難的,因此我們必須要考慮程式存在錯誤的情況。

程式中常見的錯誤:語法錯誤執行錯誤

語法錯誤在編譯時編譯系統是能夠發現的,並且編譯系統會告訴我們錯誤是什麼、位置是在哪兒,因此這樣的錯誤是比較容易發現和糾正的。

另外還有一些程式它能正常通過編譯也能執行,但是在執行過程中會出現異常,往往得不到正確的結果,甚至導致程式死亡

程式死亡的三種方式:
- 無疾而終(自然死亡,通常是return 0)
- 自殺(abort與exit)
- 他殺(系統對異常進行處理使程式終止)

我們都知道在撥打電話號碼時,若不小心撥的是空號,此時電話系統不會讓你無終止的等待,也不會簡單的掛機了事;反而它會告訴你“對不起,您撥打的號碼是空號”,這樣人們就明白出了什麼樣的錯誤,應該採取什麼樣的措施糾正。

同樣地,人們不僅希望程式能在正確的情況下執行,對於有錯的情況也能做出相應的處理,幫助我們儘快發現問題並且改正錯誤,而不致使程式莫名其妙的中止。

對異常能進行處理,這就是系統的容錯能力。

在設計程式時應當事先分析程式執行時可能出現的各種意外情況,並且分別制定出相應的處理方法這就是程式的異常處理的任務。

異常處理具體過程:在程式中設定了異常處理機制,若在執行時出現了異常,程式的流程就會跳轉到異常處理程式碼段處理。

C語言–異常處理

  1. 終止程式(除數為0)
  2. 返回一個表示錯誤的值,附加錯誤碼(GetLastError())
  3. 返回一個合法值,讓程式處於某種非法的狀態(坑爹的atoi())
  4. 呼叫一個預先準備好在出現”錯誤”的情況下用的函式(回撥函式)
  5. 暴力解決方式:abort()或者exit()
  6. 使用goto語句
  7. setjmp()和longjmp()組合
1. 終止程式(除數為0)
void FunTest()
{
    int tmp = 0;
    int i = 5 / tmp;  
}

int main()
{
    FunTest();//程式會直接崩潰

    return 0;
}
2. 返回一個表示錯誤的值,附加錯誤碼(GetLastError())
int main()
{
    FILE* pf = fopen
("1.txt", "rb"); if (NULL == pf) { cout << "error=" << errno << endl; int i = GetLastError();//獲取最近的錯誤碼 #include<windows.h> cout << "i=" << i << endl; return 0; } fclose(pf); return 0; }

輸出結果:
這裡寫圖片描述

輸出結果為2,代表了什麼意思,使用 &err,hr 在監視裡檢視錯誤碼和錯誤訊息(&err,hr的意義相當於讓偵錯程式幫你獲取GetLastError的值)或者開啟工具欄的錯誤查詢,輸入2,檢視錯誤資訊。
這裡寫圖片描述

3. 返回一個合法值,讓程式處於某種非法的狀態(坑爹的atoi())
int main()
{
    cout << atoi("-12345.67") << endl;//輸出-12345
    cout << atoi("12ab34") << endl;//輸出12

    return 0;
}

atoi() 函式掃描引數 str 字串,會跳過前面的空白字元(例如空格,tab縮排等)直到遇上數字或正負符號才開始做轉換,而再遇到非數字或字串結束時(‘\0’)才結束轉換,並將結果返回,且只適用於十進位制數字;上面的程式碼中若”12ab34”中的’ab’為字元,則輸出沒有錯;若‘ab’為十六進位制數字,則輸出就不是我們想要的結果了。

4. 呼叫一個預先準備好在出現”錯誤”的情況下用的函式(回撥函式)
void Error()
{
    printf("檔案不存在\n");
}

int main()
{
    FILE* pf = fopen("1.txt", "r");
    if (NULL == pf) {
        printf("操作無效,");//1.txt不存在,則會呼叫Error
        Error();
        system("pause");
        return 0;
    }
    printf("開啟檔案成功\n");
    fclose(pf);
    system("pause");
    return 0;

}
5. 暴力解決方式:abort()或者exit()

兩者區別:abort是直接中斷;exit是退出,會做一些清理工作

int Div(int a,int b){
    if(b==0){
        exit(1);      //直接退出
        //abort();    //abort是直接中斷,但在Windows下會彈出一個資訊框
    }
    return a/b;
}

int main(){
    printf("正常情況:4/2 = %d\n",Div(4,2));
    printf("錯誤情況:4/0 = %d\n",Div(4,0));
    return 0;
}
6. 使用goto語句

goto語句通常與條件語句配合使用來改變程式流向,使得程式轉去執行語句標號所標識的語句。

int main()
{
    FILE* pf = fopen("1.txt", "rb");
    if (NULL == pf) {
        int i = GetLastError();
        cout << "i=" << i << endl;
        goto L;//goto語句
    }
    fclose(pf);
L:  
    return 0;
}

補充一下:
若程式碼是這個樣子,使用goto語句會跳轉到百度頁面嗎?
這裡寫圖片描述
答案:並不會,編譯時也不會報錯。

編譯器的理解:
http: 是一個標籤,地位相當於L:
//www.baidu.com是一個標註,地位相當於 //goto語句
這裡寫圖片描述

7. setjmp()和longjmp()組合

非區域性跳轉語句:setjmp和longjmp函式。非區域性指的是,這不是goto語句,goto語句可以實現在一個函式內實施的跳轉,而setjmp和longjmp函式是在棧上跳過若干呼叫幀,返回到當前函式呼叫路徑上的某一個函式中。

#include <setjmp.h>
int setjmp(jmp_buf  env);
//返回值:若直接呼叫則返回0,若從longjmp呼叫則返回非0值的longjmp中的val值
Void longjmp(jmp_buf env,int val);
//呼叫此函式則返回到語句setjmp所在的地方,其中env 就是setjmp中的 env,而val 則是使setjmp的返回值變為val。

上程式碼:

#include<setjmp.h>

jmp_buf buf;//將buf定義成全域性變數,為了儲存當前資訊的執行點

void Test1()
{
    FILE* pf = fopen("1.txt", "r");
    if (NULL == pf) {
        longjmp(buf, 1);
    }
    //正常操作
    fclose(pf);
}

void Test2()
{
    int* p = (int*)malloc(sizeof(int) * 100000000);
    if (NULL == p) {
        longjmp(buf, 2);
    }
    //正常操作
    free(p);
}

int Test3(int a, int b)
{
    if (0 == b) {
        longjmp(buf, 2);
    }
    return a/b;
}

int main()
{
    int istate = setjmp(buf);//換成int istate = 0 程式會崩潰
    if (0 == istate) {
        Test1();
        Test2();
        Test3(10, 0);
    }
    else {
        switch (istate) {
        case 1:
            cout<<"開啟檔案失敗"<<endl;
            break;
        case 2:
            cout << "申請空間失敗" << endl;
            break;
        case 3:
            cout << "除數為0非法" << endl;
            break;
        default:
            cout << "未知錯誤" << endl;

        }
    }
    system("pause");
    return 0;
}

猜猜這段程式碼最後輸出的結果是什麼?
這裡寫圖片描述
沒想到吧,感覺這段程式碼只調用了Test1()函式,實際上我們通過除錯發現真的只調用了Text1()函式,Text2與Text3根本就沒有呼叫。

使用setjmp和longjmp要注意:

setjump必須先呼叫,在異常位置通過呼叫longjmp以恢復先前被儲存的程式執行點,否則將導致不可預測的結果,甚至程式崩潰

C++–異常處理

C++異常機制使用了三個的關鍵字
丟擲(throw) ──丟擲一個異常
檢查(try) ──標識可能出現的異常程式碼段
捕捉(catch) ──處理異常的程式碼段

異常,當一個函式發現自己無法處理的錯誤時會丟擲異常,讓函式的呼叫者
直接或間接的來處理這個問題。

1.異常的丟擲和捕獲

  • 異常是通過丟擲物件而引發的,該物件的型別決定了應該啟用哪個處理程式碼
  • 被選中的處理程式碼是呼叫鏈中與該物件型別匹配且離丟擲異常位置最近的那一個
  • 丟擲異常後會釋放區域性儲存物件,所以被丟擲的物件也就還給系統了,throw表示式會初始化一個丟擲特殊的異常物件副本(匿名物件),異常物件由編譯管理,異常物件在傳給對應的catch處理之後撤銷

2.棧展開

  • 丟擲異常的時候,將暫停當前函式的執行,開始查詢對應的匹配catch子句。首先檢查throw本身是否在try塊內部,如果是,再查詢匹配的catch語句。如果有匹配的,則處理;沒有則退出當前函式棧,繼續在呼叫函式的棧中進行查詢,不斷重複上述過程。
  • 若到達main函式的棧,依舊沒有匹配的,則終止程式。上述這個沿著呼叫鏈查詢匹配的catch子句的過程稱為棧展開。找到匹配的catch子句並處理以後,會繼續沿著catch子句後面繼續執行

3.異常捕獲的匹配規則

異常物件的型別與catch說明符的型別必須完全匹配。只有以下幾種情況例外:

  • 允許從非const物件到const的轉換
  • 允許從派生型別到基類型別的轉換
  • 將陣列轉換為指向陣列型別的指標,將函式轉換為指向函式型別的指標

4.異常重新丟擲

  • 有可能單個的catch不能完全處理一個異常,在進行一些校正處理以後,希望再交給更外層的呼叫鏈函式來處理,catch則可以通過重新丟擲將異常傳遞給更上層的函式進行處理