一. 回顧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... */