第9課 C++異常處理機制

一. 回顧C++異常機制ios

(一)概述編程

    1. 異常處理是C++的一項語言機制,用於在程序中處理異常事件(也被稱爲導常對象)。數組

    2. 異常事件發生時,使用throw關鍵字拋出異常表達,拋出點稱爲異常出現點,由操做系統爲程序設置當前異常對象。而後執行程序當前異常處理代碼塊。函數

    3. 在包含異常出現點的最內層try塊,依次匹配catch語句中的異常對象若匹配成功,則執行catch塊內的異常處理語句,而後接着執行try…catch…塊以後的代碼測試

    4.若是在當前try…catch…塊內找不到匹配該異常對象的catch語句,則由更外層的try…catch…塊來處理該異常。若是當前函數的全部try…catch…都不匹配該異常,則遞歸回退到調用棧的上一層去處理該異常。若是一直退出main()函數都不能處理該異常,則調用系統函數terminate()來終止程序。優化

(二)異常對象this

  1. 異常對象是一種特殊的對象。編譯器依據異常拋出表達式構造異常對象(即異常對象老是被拷貝)。對象的類型是由表達式所表示對象的靜態編譯類型決定的。如Parent& rObj = Child; throw rObj;時會拋出Parent類型的異常對象。spa

  2. 異常對象存放在內存特殊位置,該位置既不是棧也不是堆,在Windows中是放在線程信息TIB中該對象由異常機制負責建立和釋放!g++和vc下存儲區域處理略有差別)。操作系統

  3. 異常對象不一樣於函數的局部對象,局部對象在函數調用結束後就被自動銷燬,而異常對象將駐留在全部可能激活的catch語句都能訪問到的內存空間中。當異常對象與catch語句成功匹配後,在該catch語句的結束處被自動析構線程

  4.在函數中返回局部變量的指針或引用幾乎確定會形成錯誤。同理,在throw語句中拋出局部變量的指針或引用也幾乎是錯誤的。

【編程實驗】捕獲異常對象(按值、按引用和按指針)

#include <iostream>
#include <string>
using namespace std;

class MyException
{
public:
    MyException() { cout << "MyException():" << this << endl; }
    MyException(const MyException&) { cout << "MyException(const MyException&):" << this << endl; }

    ~MyException() { cout << "~MyException():" << this << endl; }

    void what() { cout << "MyException: this = " << this << endl; }
};

class MyChildExcept : public MyException
{
public:
    MyChildExcept() { cout << "MyChildExcept():" << this << endl; }
    MyChildExcept(const MyChildExcept&) { cout << "MyChildExcept(const MyChildExcept&):" << this << endl; }

    ~MyChildExcept() { cout << "~MyChildExcept():" << this << endl; }

    void what() { cout << "MyChildExcept: this = " << this << endl; }
};

void func_local()
{
    // throw 局部對象
    MyException localEx;
    throw localEx;   //儘管localEx是個局部對象,但這裏會將其複製構造出一個異常對象,並存儲在TIB中。而不是真正的將局部對象拋出去!
}

void func_temp()
{
    //throw 臨時對象
    MyException();       //臨時對象1
    throw MyException(); //編譯器會將這個臨時對象直接存儲在線程TIB中,成爲異常對象(注意與臨時對象1存儲位置通常相距較遠!)
}

void func_ptr()
{
    //throw 指針
    throw new MyException(); //注意:異常對象是複製的堆對象而來的指針(存在內存泄漏風險!!!)
}

void func_again()
{
    MyChildExcept child;
    MyException& re = child; //注意拋出的是re的靜態類型的異常對象,即MyException,而不是MyChildExcept;
    throw re;
}

int main()
{
    cout << "----------------------------------catch by value------------------------------" << endl;
    //按值捕獲
    try {
        func_local();        //throw MyExecption()
    }
    catch (MyException e) {  //複製異常對象,須額外進行一次拷貝!
        cout << "catch (MyException e)" << endl;
        e.what();
    }

    cout << "--------------------------------catch by reference----------------------------" << endl;
    //按引用捕獲
    try {
        func_temp();
    }
    catch (MyException& e) { //直接引用異常對象,無須拷貝
        cout << "catch (MyException& e)" << endl;
        e.what();
    }

    cout << "---------------------------------catch by pointer-----------------------------" << endl;
    //按指針捕獲
    try {
        func_ptr();
    }
    catch (MyException* e) { //按指針捕獲(可能形成內存泄漏)
        cout << "catch (MyException* e)" << endl;
        e->what();
        delete e;  //釋放堆對象,防止內存泄漏
    }

    cout << "------------------------------------throw again-------------------------------" << endl;
    //二次拋異常
    try {
        try {
            func_again();
        }
        catch (MyException& e) {
            e.what();

            //注意如下兩種方式不一樣
            //1. 在throw後指定異常對象爲e
            //throw e; //e會繼續複製一份,並拋出複製的異常對象而e自己會被釋放!

            //2.throw後不指定任何對象,只要是在catch中捕獲的,一概拋出去。
            throw;    //此時,e自己再被拋出去。不會另外構造異常對象。
        }
    }
    catch (MyException& e) {
        e.what();
    }

    return 0;
}


/*輸出結果(g++編譯環境下的輸出)
----------------------------------catch by value------------------------------
MyException():0x61fe7f    //localEx對象
MyException(const MyException&):0x29978f8   //throw時將localEx複製給異常對象
~MyException():0x61fe7f   //釋放localEx
MyException(const MyException&):0x61feaf    //將異常對象複製給catch中的e對象
catch (MyException e)
MyException: this = 0x61feaf
~MyException():0x61feaf      //釋放catch中的e對象
~MyException():0x29978f8     //釋放異常對象
--------------------------------catch by reference----------------------------
MyException():0x61fe7f   //建立臨時對象1
~MyException():0x61fe7f  //釋放臨時對象1
MyException():0x29978f8  //throw MyException()時,會將這個臨時對象直接建立在TIB中,與臨時對象1不在同一地址段中
catch (MyException& e)   //按引用傳遞,e直接引用異常對象,少了一次拷貝
MyException: this = 0x29978f8
~MyException():0x29978f8  //釋放異常對象
---------------------------------catch by pointer-----------------------------
MyException():0x299c638   //throw new Exception() ,先在堆中建立一個Exception對象,再將指針throw出去。
catch (MyException* e)
MyException: this = 0x299c638
~MyException():0x299c638   //手動調用delete後,釋放堆中的Exception對象。
------------------------------------throw again-------------------------------
MyException():0x61fe7b
MyChildExcept():0x61fe7b
MyException(const MyException&):0x29978f8  //異常對象,這裏是re的靜態類型,即MyException,而不是MyChildExcept!!!
~MyChildExcept():0x61fe7b
~MyException():0x61fe7b
MyException: this = 0x29978f8  //內層catch到的異常對象
MyException: this = 0x29978f8  //外層catch到的異常對象,注意與內層是同一對象
~MyException():0x29978f8   //釋放異常對象
*/

2、異常規格

(一)C++0x與C++11異常規格聲明方式的不一樣

  1. void func() throw() { ... } // throw()聲明該函數不會產生異常(C++0x)

  2. void func() throw(int, double) { ... } //可能產生int或double類型異常(C++0x)

  3. void func() noexcept { ... } // noexcept聲明該函數不會產生異常(C++11)

  4. void func() noexcept(常量表達式) { ... } //由表達式決定是否產生異常(C++11)

(二)noexcept和throw()異常規格聲明的區別

  1. 當函數後面加noexcept和throw()時均表示該函數不會產生異常。

  2. 當使用throw()這種傳統風格聲明時,若是函數拋出了異常,異常處理機制會進行棧回溯,尋找(一個或多個)catch語句來匹配捕獲。若是沒有匹配的類型,會調用std::unexcepted函數,可是std::unexcepted自己也可能拋出異常,若是它拋出的異常對當前的異常規格是有效的,異常傳遞和棧回溯會像之前那樣繼續進行。這意味着若是使用throw()來聲明,編譯器幾乎沒有機會作優化,甚至會產生的代碼會很臃腫、龐大。由於:

    ①棧必須保存在回退表中;

    ②全部對象的析構函數必須被正確調用(按照對象構建相反的順序析構對象)。

    ③編譯器可能引入新的傳播柵欄(propagation barriers)、引入新的異常表入口,使得異常處理的代碼變得龐大。

    ④內聯函數的異常規格可能無效。

    3. 當使用noexcept時,std::terminate()函數會被當即調用,而不是調用std::unexcepted()所以棧不回溯,這爲編譯器的優化提供了空間

    4. 總之,若是知道函數絕對不會拋出任何異常,應該使用noexcept,而不是throw() 。

3、noexcept關鍵字

(一)noexcept異常規格語法

  1. noexcept()聲明函數不會拋出任何異常。(注意throw()聲明不拋出動態異常時,會保證進行棧回溯,可能調用std::unexcepted)。

  2. noexcept(true)、noexcept(false)前者與noexcept()等價,後者表示函數可能拋出異常。

  3. noexcept(表達式):其中的表達式是可按語境轉換爲bool類型的常量表達式。若表達式求值爲true,則聲明函數爲不拋出任何異常。若爲false則表示函數可能拋出異常。

(二)noexcept在函數指針、虛函數中的使用規則

  1. noexcept與函數指針:

    (1)規則1:若是爲函數指針顯式聲明瞭noexcept規格,則該指針只能指向帶有noexcept的同種規格函數。

    (2)規則2:若是未聲明函數指針的異常規則(即隱式說明可能拋異常),則該指針能夠指向任何函數(即帶noexcept或不帶noexcept函數)。

  2. noexcept與虛函數:

    (1)若是基類虛函數聲明不拋異常,則子類也必須作出相同的承諾。即子類也須帶上noexcept。

    (2)若是基類虛函數聲明可能拋異常,則子類能夠拋異常,也能夠不拋異常。

 (三)注意事項

  1. noexcept聲明是函數接口的組成部分,這意味着調用方可能會對它有依賴。(但不能依靠noexcept來構成函數的重載)

  2. 相對於不帶noexcept聲明的函數,帶有noexcept聲明的函數有更多機會獲得優化。但大多數函數是異常中立的,即不具有noexcept性質。

  3. 不拋異常的函數,容許調用潛在拋出異常的函數。異常若是沒有被阻止傳播,最終會調用std::terminate來終止程序。

  4. noexcept對於移動操做、swap、內存釋放函數和析構函數最有價值默認地,operator delete、operator delete[]和析構函數都隱式具有了noexcept性質,但能夠從新顯式指定爲可能拋出異常。

【編程實驗】noexcept測試

#include <iostream>
#include <type_traits>
using namespace std;

//如下兩個函數不能構成重載
//void func(int) noexcept(true){}
//void func(int) noexcept(false){}

//1. 異常規格與函數指針
void func1(int) noexcept(true) {};        //不拋出異常
void func2(int) noexcept(false) {}; //可能拋出異常
void func3(int){}

//2. noexcept與虛函數
class Parent
{
public:
    virtual void func1() noexcept {}      //不拋異常
    virtual void func2() noexcept(false) {} //可能拋異常
};

class Child : public Parent
{
public:
    //基類承諾不拋異常,則子類也必須承諾!
    //void func1() {};  //error, Parent::func1() 承諾不拋異常了

    //基類聲明爲可能拋異常,則子類能夠拋,也能夠不拋異常
    void func2() noexcept(true) {} //ok, 子類不拋異常
    //void func2() {};     //ok,子類能夠拋異常
};

//3. 有條件的noexcept
//3.1 Swap:交互兩個數組的元素。
//只要交互元素時不拋異常,但交換整個數組就不拋異常。所以Swap函數是否爲noexcept取決於交互元素
template<typename T, size_t N>
void Swap(T(&a)[N], T(&b)[N]) noexcept(noexcept(std::swap(*a, *b))) //*a、*b爲首元素
{
    for (int i = 0; i < N; ++i) {
        std::swap(a[i], b[i]);
    }
}
template<typename T, size_t N>
void printArray(T(&a)[N])
{
    for (int i = 0; i < N; ++i) {
        cout << a[i] << " ";
    }
    cout << endl;
}

//3.2 func
//func_destructor函數拋不拋異常取決於T的析構函數。若是T析構會拋異常則func_destructor也會拋異常,反之不拋異常。
template<typename T>
void func_destructor(const T&) noexcept(noexcept((std::declval<T>().~T()))) {} 

struct CA
{
    ~CA(){ throw 1; } //注意析構函數默認爲noexcept(true)
};

struct CB
{
    ~CB()noexcept(false){ throw 2; }
};

struct CC
{
    CB b;
};

//3.3 pair
template<class T1, class T2>
class MyPair
{
    T1 first;
    T2 second;
public:

    //swap函數是否爲noexcept,取決於交互first和second的過程是否爲noexcept
    void swap(MyPair& p) noexcept(noexcept(std::swap(first, p.first)) &&
                                  noexcept(std::swap(second, p.second)))
    {
    }
};

//4. noexcept
void Throw() { throw 1; }        //可能拋異常:異常會被向外層傳遞出去
void NoBlockThrow() { Throw(); } //可能拋異常:異常會被向外層傳遞出去
void BlockThrow() noexcept { Throw(); } //不拋異常,但此函數實際會拋異常,異常會阻止傳遞,程序停止

int main()
{
    //1. noexcept與函數指針
    //1.1規則1:
    void(*pf1)(int) noexcept = func1; //正確
    //void(*pf2)(int) noexcept = func2; //error;異常規格不一樣!
    //void(*pf3)(int) noexcept = func3; //error,帶noexcept的指針,只能指向帶同種noexcept規格的函數
    
    //1.2 規則2:
    void(*pf4)(int) = func1;  //or,pf3未帶noexcept,能夠指向任何函數
    void(*pf5)(int) = func2;  //ok
    void(*pf6)(int) = func3;  //ok

    //2. noexcept與虛函數(見Child類)

    //3. 有條件的noexcept

    //3.1 交換數組
    int a[] = { 1,2,3,4,5 };
    int b[] = { 6,7,8,9,10 };

    Swap(a, b); //Swap函數是否爲noexcept,取決於std::swap(*a,*b)函數是否爲noexcept
    printArray(a);
    printArray(b);

    //3.2 析構函數
    try {
        CB temp;
        func_destructor(temp); //分別測試CA, CB, CC類。此處,因爲CB析構函數爲可能拋出異常,所以
                               //拋出會被繼續拋出而被catch(...)語句捕獲,程序不會被停止
    }
    catch (...) {
        cout << "catch destructor exception" << endl;
    }

    //4. noexcept與異常傳遞測試
    try {
        Throw(); //這裏可增長測試NoBlockThrow、BlockThrow函數
    }catch (...) {
        cout <<"catch exception..." << endl;
    }
    
    return 0;
}
/*輸出結果:
6 7 8 9 10
1 2 3 4 5
catch destructor exception
catch exception...
*/
相關文章
相關標籤/搜索