C++例外處理(try catch throw)完全攻略

2020-07-16 10:04:42
程式執行時常會碰到一些異常情況,例如:
  • 做除法的時候除數為 0;
  • 使用者輸入年齡時輸入了一個負數;
  • 用 new 運算子動態分配空間時,空間不夠導致無法分配;
  • 存取陣列元素時,下標越界;開啟檔案讀取時,檔案不存在。

這些異常情況,如果不能發現並加以處理,很可能會導致程式崩潰。

所謂“處理”,可以是給出錯誤提示資訊,然後讓程式沿一條不會出錯的路徑繼續執行;也可能是不得不結束程式,但在結束前做一些必要的工作,如將記憶體中的資料寫入檔案、關閉開啟的檔案、釋放動態分配的記憶體空間等。

一發現異常情況就立即處理未必妥當,因為在一個函數執行過程中發生的異常,在有的情況下由該函數的呼叫者決定如何處理更加合適。尤其像庫函數這類提供給程式設計師呼叫,用以完成與具體應用無關的通用功能的函數,執行過程中貿然對異常進行處理,未必符合呼叫它的程式的需要。

此外,將異常分散在各處進行處理不利於程式碼的維護,尤其是對於在不同地方發生的同一種異常,都要編寫相同的處理程式碼也是一種不必要的重複和冗餘。如果能在發生各種異常時讓程式都執行到同一個地方,這個地方能夠對異常進行集中處理,則程式就會更容易編寫、維護。

鑑於上述原因,C++ 引入了例外處理機制。其基本思想是:函數 A 在執行過程中發現異常時可以不加處理,而只是“拋出一個異常”給 A 的呼叫者,假定為函數 B。

拋出異常而不加處理會導致函數 A 立即中止,在這種情況下,函數 B 可以選擇捕獲 A 拋出的異常進行處理,也可以選擇置之不理。如果置之不理,這個異常就會被拋給 B 的呼叫者,以此類推。

如果一層層的函數都不處理異常,異常最終會被拋給最外層的 main 函數。main 函數應該處理異常。如果main函數也不處理異常,那麼程式就會立即異常地中止。

C++例外處理基本語法

C++ 通過 throw 語句和 try...catch 語句實現對異常的處理。throw 語句的語法如下:

throw  表示式;

該語句拋出一個異常。異常是一個表示式,其值的型別可以是基本型別,也可以是類。

try...catch 語句的語法如下:

try {
    語句組
}
catch(異常型別) {
    例外處理程式碼
}
...
catch(異常型別) {
    例外處理程式碼
}

catch 可以有多個,但至少要有一個。

不妨把 try 和其後{}中的內容稱作“try塊”,把 catch 和其後{}中的內容稱作“catch塊”。

try...catch 語句的執行過程是:
  • 執行 try 塊中的語句,如果執行的過程中沒有異常拋出,那麼執行完後就執行最後一個 catch 塊後面的語句,所有 catch 塊中的語句都不會被執行;
  • 如果 try 塊執行的過程中拋出了異常,那麼拋出異常後立即跳轉到第一個“異常型別”和拋出的異常型別匹配的 catch 塊中執行(稱作異常被該 catch 塊“捕獲”),執行完後再跳轉到最後一個 catch 塊後面繼續執行。

例如下面的程式:
#include <iostream>
using namespace std;
int main()
{
    double m ,n;
    cin >> m >> n;
    try {
        cout << "before dividing." << endl;
        if( n == 0)
            throw -1; //丟擲int型別異常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch(double d) {
        cout << "catch(double) " << d <<  endl;
    }
    catch(int e) {
        cout << "catch(int) " << e << endl;
    }
    cout << "finished" << endl;
    return 0;
}
程式的執行結果如下:
9 6↙
before dividing.
1.5
after dividing.
finished

說明當 n 不為 0 時,try 塊中不會拋出異常。因此程式在 try 塊正常執行完後,越過所有的 catch 塊繼續執行,catch 塊一個也不會執行。

程式的執行結果也可能如下:
9 0↙
before dividing.
catch(int) -1
finished

當 n 為 0 時,try 塊中會拋出一個整型異常。拋出異常後,try 塊立即停止執行。該整型異常會被型別匹配的第一個 catch 塊捕獲,即進入catch(int e)塊執行,該 catch 塊執行完畢後,程式繼續往後執行,直到正常結束。

如果拋出的異常沒有被 catch 塊捕獲,例如,將catch(int e),改為catch(char e),當輸入的 n 為 0 時,拋出的整型異常就沒有 catch 塊能捕獲,這個異常也就得不到處理,那麼程式就會立即中止,try...catch 後面的內容都不會被執行。

能夠捕獲任何異常的 catch 語句

如果希望不論拋出哪種型別的異常都能捕獲,可以編寫如下 catch 塊:

catch(...) {
    ...
}

這樣的 catch 塊能夠捕獲任何還沒有被捕獲的異常。例如下面的程式:
#include <iostream>
using namespace std;
int main()
{
    double m, n;
    cin >> m >> n;
    try {
        cout << "before dividing." << endl;
        if (n == 0)
            throw - 1;  //丟擲整型異常
        else if (m == 0)
            throw - 1.0;  //拋出 double 型異常
        else
            cout << m / n << endl;
        cout << "after dividing." << endl;
    }
    catch (double d) {
        cout << "catch (double)" << d << endl;
    }
    catch (...) {
        cout << "catch (...)" << endl;
    }
    cout << "finished" << endl;
    return 0;
}
程式的執行結果如下:
9 0↙
before dividing.
catch (...)
finished

當 n 為 0 時,拋出的整型異常被catchy(...)捕獲。

程式的執行結果也可能如下:
0 6↙
before dividing.
catch (double) -1
finished

當 m 為 0 時,拋出一個 double 型別的異常。雖然catch (double)catch(...)都能匹配該異常,但是catch(double)是第一個能匹配的 catch 塊,因此會執行它,而不會執行catch(...)塊。

由於catch(...)能匹配任何型別的異常,它後面的 catch 塊實際上就不起作用,因此不要將它寫在其他 catch 塊前面。

異常的再拋出

如果一個函數在執行過程中拋出的異常在本函數內就被 catch 塊捕獲並處理,那麼該異常就不會拋給這個函數的呼叫者(也稱為“上一層的函數”);如果異常在本函數中沒有被處理,則它就會被拋給上一層的函數。例如下面的程式:
#include <iostream>
#include <string>
using namespace std;
class CException
{
public:
    string msg;
    CException(string s) : msg(s) {}
};
double Devide(double x, double y)
{
    if (y == 0)
        throw CException("devided by zero");
    cout << "in Devide" << endl;
    return x / y;
}
int CountTax(int salary)
{
    try {
        if (salary < 0)
            throw - 1;
        cout << "counting tax" << endl;
    }
    catch (int) {
        cout << "salary < 0" << endl;
    }
    cout << "tax counted" << endl;
    return salary * 0.15;
}
int main()
{
    double f = 1.2;
    try {
        CountTax(-1);
        f = Devide(3, 0);
        cout << "end of try block" << endl;
    }
    catch (CException e) {
        cout << e.msg << endl;
    }
    cout << "f = " << f << endl;
    cout << "finished" << endl;
    return 0;
}
程式的輸出結果如下:
salary < 0
tax counted
devided by zero
f=1.2
finished

CountTa 函數拋出異常後自行處理,這個異常就不會繼續被拋給呼叫者,即 main 函數。因此在 main 函數的 try 塊中,CountTax 之後的語句還能正常執行,即會執行f = Devide(3, 0);

第 35 行,Devide 函數拋出了異常卻不處理,該異常就會被拋給 Devide 函數的呼叫者,即 main 函數。拋出此異常後,Devide 函數立即結束,第 14 行不會被執行,函數也不會返回一個值,這從第 35 行 f 的值不會被修改可以看出。

Devide 函數中拋出的異常被 main 函數中型別匹配的 catch 塊捕獲。第 38 行中的 e 物件是用複製建構函式初始化的。

如果拋出的異常是派生類的物件,而 catch 塊的異常型別是基礎類別,那麼這兩者也能夠匹配,因為派生類物件也是基礎類別物件。

雖然函數也可以通過返回值或者傳參照的引數通知呼叫者發生了異常,但採用這種方式的話,每次呼叫函數時都要判斷是否發生了異常,這在函數被多處呼叫時比較麻煩。有了例外處理機制,可以將多處函數呼叫都寫在一個 try 塊中,任何一處呼叫發生異常都會被匹配的 catch 塊捕獲並處理,也就不需要每次呼叫後都判斷是否發生了異常。

有時,雖然在函數中對異常進行了處理,但是還是希望能夠通知呼叫者,以便讓呼叫者知道發生了異常,從而可以作進一步的處理。在 catch 塊中拋出異常可以滿足這種需要。例如:
#include <iostream>
#include <string>
using namespace std;
int CountTax(int salary)
{
    try {
        if( salary < 0 )
            throw string("zero salary");
        cout << "counting tax" << endl;

    }
    catch (string s ) {
        cout << "CountTax error : " << s << endl;
        throw; //繼續丟擲捕獲的異常
    }
    cout << "tax counted" << endl;
    return salary * 0.15;
}
int main()
{
    double f = 1.2;
    try {
        CountTax(-1);
        cout << "end of try block" << endl;
    }
    catch(string s) {
        cout << s << endl;
    }
    cout << "finished" << endl;
    return 0;
}
程式的輸出結果如下:
CountTax error:zero salary
zero salary
finished

第 14 行的throw;沒有指明拋出什麼樣的異常,因此拋出的就是 catch 塊捕獲到的異常,即 string("zero salary")。這個異常會被 main 函數中的 catch 塊捕獲。

函數的異常宣告列表

為了增強程式的可讀性和可維護性,使程式設計師在使用一個函數時就能看出這個函數可能會拋出哪些異常,C++ 允許在函數宣告和定義時,加上它所能拋出的異常的列表,具體寫法如下:

void func() throw (int, double, A, B, C);

void func() throw (int, double, A, B, C){...}

上面的寫法表明 func 可能拋出 int 型、double 型以及 A、B、C 三種型別的異常。異常宣告列表可以在函數宣告時寫,也可以在函數定義時寫。如果兩處都寫,則兩處應一致。

如果異常宣告列表如下編寫:

void func() throw ();

則說明 func 函數不會拋出任何異常。

一個函數如果不交待能拋出哪些型別的異常,就可以拋出任何型別的異常。

函數如果拋出了其異常宣告列表中沒有的異常,在編譯時不會引發錯誤,但在執行時, Dev C++ 編譯出來的程式會出錯;用 Visual Studio 2010 編譯出來的程式則不會出錯,異常宣告列表不起實際作用。

C++標準異常類

C++ 標準庫中有一些類代表異常,這些類都是從 exception 類派生而來的。常用的幾個異常類如圖 1 所示。


圖1:常用的異常類