C++異常相關
使用異常處理,程序中獨立開發的各部分能夠就程序執行期間出現的問題相互通信,並處理這些問題。C++ 的異常處理中,需要由問題檢測部分拋出一個對象給處理代碼,通過這個對象的類型和內容,兩個部分能夠就出現了什麽錯誤進行通信。
一:概述
1:在C++中的異常處理語句包括:
try:try語句塊以try關鍵字開始,並且以一個或者多個catch子句結束。try語句塊中執行正常的代碼,並且可以拋出異常,在try塊後面catch子句捕獲異常,並處理之。
throw表達式:用來拋出異常。
try塊的通用語法形式是:
try { program-statements } catch(exception-specifier) { handler-statements; } catch(…) { … }
2:catch後的圓括號內是單個類型或者單個對象的聲明,稱為異常說明符,如果catch捕獲了異常,則執行相關的塊語句,一旦這個catch子語句執行結束,則程序流程跳轉到緊跟最後一個catch子句的語句。
throw表達式的類型決定了拋出異常的類型。在拋出異常之後,如果不存在處理該異常的catch子句,則程序的運行就跳轉到名為terminate的標準庫函數,一般該函數的執行導致程序的非正常退出。而且,如果程序不是在try中拋出的異常,則也會執行terminate函數,比如:
int a = 3; try { if(a == 3) { throw 3.0; } } catch(int b) { cout << b << endl; }
代碼中拋出的類型為double型,但是catch捕獲int類型的異常,所以,程序打印:
terminate called after throwing an instance of ‘double‘ Aborted (core dumped)
如果改為throw 3的話,則輸出“3”
下面的代碼:
int a = 3; throw 3.0;
同樣打印:
terminate called after throwing an instance of ‘double‘ Aborted (core dumped)
3:標準庫異常類的繼承關系如下:
exception 類型所定義的唯一操作是一個名為 what 的虛成員,該函數返回const char* 對象,它一般返回用來在拋出位置構造異常對象的信息。
應用程序還經常通過從exception 類或者中間基類派生附加類型來擴充 exception 層次。
二:異常的引發
1:異常是通過拋出對象而引發的。該對象的類型決定應該激活哪個處理代碼。被選中的處理代碼是調用鏈中與該對象類型匹配且離拋出異常位置最近的那個。
2:異常以類似於將實參傳遞給函數的方式拋出和捕獲。
3:傳遞數組或函數類型實參的時候,該實參自動轉換為一個指針。被拋出的異常對象將發生同樣的自動轉換。
4:執行 throw 的時候,不會執行跟在 throw 後面的語句,而是將控制從 throw 轉移到匹配的 catch,該 catch 可以是同一函數中局部的 catch,也可以在直接或間接調用發生異常的函數的另一個函數中。控制從一個地方傳到另一地方,這意味著:
A:沿著調用鏈的函數提早退出;
B:一般而言,在處理異常的時候,拋出異常的塊中的局部存儲不存在了。
5:因為在處理異常的時候會釋放局部存儲,所以被拋出的對象就不能再局部存儲,而是用 throw 表達式初始化一個稱為異常對象的特殊對象。異常對象由編譯器管理,而且保證駐留在可能被激活的任意 catch 都可以訪問的空間。這個對象由 throw 創建,並被初始化為被拋出的表達式的副本。異常對象將傳給對應的 catch,並且在完全處理了異常之後撤銷。
因此,異常對象通過復制被拋出表達式的結果創建,該結果必須是可以復制的類型。當拋出一個表達式的時候,被拋出對象的靜態編譯時類型將決定異常對象的類型。
6:如果在拋出中需要對指針解引用,則指針解引用的結果是一個對象,其類型與指針的類型匹配。如果指針指向繼承層次中的一種類型,指針所指對象的類型就有可能與指針的類型不同。無論對象的實際類型是什麽,異常對象的類型都與指針的靜態類型相匹配。如果該指針是一個指向派生類對象的基類類型指針,則那個對象將被分割,只拋出基類部分。
如果拋出指針本身,可能會引發比分割對象更嚴重的問題。具體而言,拋出指向局部對象的指針總是錯誤的,其理由與從函數返回指向局部對象的指針是錯誤的一樣。
7:拋出異常的時候,將暫停當前函數的執行,開始查找匹配的 catch 子句。首先檢查 throw 本身是否在 try 塊內部,如果是,檢查與該 catch 相關的catch 子句,看是否其中之一與拋出對象相匹配。如果找到匹配的 catch,就處理異常;如果找不到,就退出當前函數(釋放當前函數的內在並撤銷局部對象),並且繼續在調用函數中查找。
如果對拋出異常的函數的調用是在 try 塊中,則檢查與該 try 相關的catch 子句。如果找到匹配的 catch,就處理異常;如果找不到匹配的 catch,調用函數也退出,並且繼續在調用這個函數的函數中查找。
這個過程,稱之為棧展開(stack unwinding),沿嵌套函數調用鏈繼續向上,直到為異常找到一個 catch 子句。只要找到能夠處理異常的 catch 子句,就進入該 catch 子句,並在該處理代碼中繼續執行。當 catch 結束的時候,在緊接在與該 try 塊相關的最後一個 catch 子句之後的點繼續執行。
棧展開期間,提早退出包含 throw 的函數和調用鏈中可能的其他函數。一般而言,這些函數已經創建了可以在退出函數時撤銷的局部對象。因異常而退出函數時,編譯器保證適當地撤銷局部對象:
class test { public: ~ test() { cout << "this is test destructor" << endl; } }; void fun3() { test tt; throw runtime_error("hehehe"); cout << "this is fun3" << endl; } void fun2() { fun3(); cout << "this is fun2" << endl; } void fun1() { try { fun2(); } catch(...) { cout << "get the exception" << endl; } cout << "this is fun1" << endl; } int main() { fun1(); cout << "this is main" << endl; }
結果是:
this is test destructor get the exception this is fun1 this is main
如果一個塊通過 new 動態分配內存,而且在釋放資源之前發生異常,在棧展開期間將不會釋放該資源。
在為某個異常進行棧展開的時候,如果析構函數又拋出自己的未經處理的另一個異常,將會導致調用標準庫 terminate 函數。一般而言,terminate函數將調用 abort 函數,強制從整個程序非正常退出。
不能不處理異常,如果找不到匹配的 catch,程序就調用庫函數 terminate。
三:捕獲異常
1:catch 子句中的異常說明符看起來像只包含一個形參的形參表。
說明符的類型決定了處理代碼能夠捕獲的異常種類。當 catch 為了處理異常只需要了解異常的類型的時候,異常說明符可以省略形參名;如果處理代碼需要已發生異常的類型之外的信息,則異常說明符就包含形參名,catch 使用這個名字訪問異常對象。
在查找匹配的 catch 期間,找到的 catch 不必是與異常最匹配的那個catch,相反,將選中第一個找到的可以處理該異常的 catch。因此,在 catch 子句列表中,最特殊的 catch 必須最先出現。
異常與 catch 異常說明符匹配的規則比匹配實參和形參類型的規則更嚴格,大多數轉換都不允許——除下面幾種可能的區別之外,異常的類型與 catch 說明符的類型必須完全匹配:
? 允許從非 const 到 const 的轉換。也就是說,非 const 對象的 throw可以與指定接受 const 引用的 catch 匹配。
? 允許從派生類型型到基類類型的轉換。
? 將數組轉換為指向數組類型的指針,將函數轉換為指向函數類型的適當指針。
在查找匹配 catch 的時候,不允許其他轉換。具體而言,既不允許標準算術轉換,也不允許為類類型定義的轉換。比如:
short a = 3; try { throw a; } catch(int a) { cout << "catch int: " << a << endl; } catch(double a) { cout << "catch double: " << a << endl; } catch(short a) { cout << "catch short: " << a << endl; }
結果是:
catch short: 3
2:進入 catch 的時候,用異常對象初始化 catch 的形參。
如果異常說明符不是引用,就將異常對象復制到 catch 形參中,對形參所做的任何改變都只作用於副本,不會作用於異常對象本身。如果說明符是引用,則像引用形參一樣,對 catch 形參所做的改變作用於異常對象。
像形參聲明一樣,基類的異常說明符可以用於捕獲派生類型的異常對象,而且,異常說明符的靜態類型決定 catch 子句可以執行的動作。如果被拋出的異常對象是派生類類型的,但由接受基類類型的 catch 處理,那麽,catch 不能使用派生類特有的任何成員。
如果 catch 形參是引用類型,catch 對象就直接訪問異常對象,catch 對象的靜態類型可以與 catch 對象所引用的異常對象的動態類型不同。如果異常說明符不是引用,則 catch 對象是異常對象的副本,如果 catch 對象是基類類型對象而異常對象是派生類型的,就將異常對象分割為它的基類子對象。
3:有可能單個 catch 不能完全處理一個異常。在進行了一些校正行動之後,catch 可以通過重新拋出將異常傳遞函數調用鏈中更上層的函數。重新拋出是後面不跟類型或表達式的一個 throw;
空 throw 語句將重新拋出異常對象,它只能出現在 catch 或者從 catch調用的函數中。如果在處理代碼不活動時碰到空 throw,就調用 terminate 函數。
重新拋出不指定自己的異常,仍然將異常對象沿鏈向上傳遞。被拋出的異常是原來的異常對象,而不是 catch 形參。當 catch 形參是基類類型的時候,我們不知道由重新拋出表達式拋出的實際類型,該類型取決於異常對象的動態類型,而不是 catch 形參的靜態類型。例如,來自帶基類類型形參 catch的重新拋出,可能實際拋出一個派生類型的對象。
一般而言,catch 可以改變它的形參。在改變它的形參之後,如果 catch 重新拋出異常,那麽,只有當異常說明符是引用的時候,才會傳播那些改變。
catch (my_error &eObj) { // specifier is a reference type eObj.status = severeErr; // modifies the exception object throw; // the status member of the exception object is severeErr } catch (other_error eObj) { // specifier is a nonreference type eObj.status = badErr; // modifies local copy only throw; // the status member of the exception rethrown is unchanged }
4:捕獲所有異常的 catch 子句形式如下:
// matches any exception that might be thrown catch (...) { // place our code here }
捕獲所有異常的 catch 子句與任意類型的異常都匹配。
catch(...) 經常與重新拋出表達式結合使用,catch 完成可做的所有局部工作,然後重新拋出異常:
void manip() { try { // actions that cause an exception to be thrown } catch (...) { // work to partially handle the exception throw; } }
5:構造函數函數體內部的 catch 子句不能處理在構造函數初始化時發生的異常。為了處理這種異常,必須將構造函數編寫為:函數 try 塊。形式如下:
template <class T> Handle<T>::Handle(T *p) try : ptr(p), use(new size_t(1)) { // empty function body } catch(const std::bad_alloc &e) { handle_out_of_memory(e); }
關鍵字 try 出現在成員初始化列表之前,並且測試塊的復合語句包圍了構造函數的函數體。catch 子句既可以處理從成員初始化列表中拋出的異常,也可以處理從構造函數函數體中拋出的異常。
四:資源分配即初始化
1:因為異常的發生,可能導致使用new申請的內存無法得到釋放:
void f() { vector<string> v; // local vector string s; while (cin >> s) v.push_back(s); // populate the vector string *p = new string[v.size()]; // dynamic array // remaining processing // it is possible that an exception occurs in this code // function cleanup is bypassed if an exception occurs delete [] p; } // v destroyed automatically when the function exits
這個函數定義了一個局部 vector 並動態分配了一個數組。在正常執行的情況下,數組和 vector 都在退出函數之前被撤銷,函數中最後一個語句釋放數組,在函數結束時自動撤銷 vector。
不管何時發生異常,都保證運行 vector 析構函數。但是,如果在 new 之後但在 delete 之前發生了異常,則使得數組無法被撤銷。
2:通過定義一個類來封閉資源的分配和釋放,可以保證正確釋放資源。這一技術常稱為“資源分配即初始化”,簡稱 RAII。
應該設計資源管理類,以便構造函數分配資源而析構函數釋放資源。想要分配資源的時候,就定義該類類型的對象。如果不發生異常,就在獲得資源的對象超出作用域的進修釋放資源。更為重要的是,如果在創建了對象之後但在它超出作用域之前發生異常,那麽,編譯器保證撤銷該對象。
3:標準庫的 auto_ptr 類是“資源分配即初始化”技術的例子。auto_ptr 類是接受一個類型形參的模板,auto_ptr 類在頭文件 memory 中定義。
每個 auto_ptr 對象指向一個對象。當 auto_ptr 對象指向一個對象的時候,可以說它“擁有”該對象。當 auto_ptr 對象超出作用域或者另外撤銷的時候,就自動回收 auto_ptr 所指向的動態分配對象。
auto_ptr 只能用於管理從 new 返回的一個對象,它不能管理動態分配的數組。auto_ptr 被復制或賦值的時候,有不尋常的行為,因此,不能將 auto_ptrs 存儲在標準庫容器類型中。
使用常規指針的形式如下:
void f() { int *ip = new int(42); // dynamically allocate a new object // code that throws an exception that is not caught inside f delete ip; // return the memory before exiting }
如果使用auto_ptr類,則形式如下:
void f() { auto_ptr<int> ap(new int(42)); // allocate a new object // code that throws an exception that is not caught inside f }// auto_ptr freed automatically when function ends
編譯器保證在展開棧越過 f 之前運行 ap 的析構函數。
希望訪問 string 操作。用普通 string 指針,像下面這樣做:
string *pstr_type = new string("Brontosaurus"); if (pstr_type->empty()) // oops, something wrong
auto_ptr 類定義了解引用操作符(*)和箭頭操作符(->)的重載版本,所以可以用類似於使用內置指針的方式使用 auto_ptr 對象:
auto_ptr<string> ap1(new string("Brontosaurus")); // normal pointer operations for dereference and arrow *ap1 = "TRex"; // assigns a new value to the object to which ap1 points string s = *ap1; // initializes s as a copy of the object to which ap1 points if (ap1->empty()) // runs empty on the string to which ap1 points
auto_ptr 的主要目的,在保證自動刪除 auto_ptr 對象引用的對象的同時,支持普通指針式行為。
如果不給定初始式,auto_ptr 對象是未綁定的,它不指向對象。對未綁定的 auto_ptr 對象解引用,其效果與對未綁定的指針解引用相同——程序出錯。
為了檢查指針是否未綁定,可以在條件中直接測試指針,效果是確定指針是否為 0。相反,不能直接測試 auto_ptr 對象:
// error: cannot use an auto_ptr as a condition if (p_auto) *p_auto = 1024;
auto_ptr 類型沒有定義到可用作條件的類型的轉換,相反,要測試auto_ptr 對象,必須使用它的 get 成員,該成員返回包含在 auto_ptr 對象中的基礎指針:
// revised test to guarantee p_auto refers to an object if (p_auto.get()) *p_auto = 1024;
應該只用 get 詢問 auto_ptr 對象或者使用返回的指針值,不能用 get 作為創建其他 auto_ptr 對象的實參。使用 get 成員初始化其他 auto_ptr 對象違反 auto_ptr 類設計原則。
auto_ptr 對象與內置指針的另一個區別是,不能直接將一個地址(或者其他指針)賦給 auto_ptr 對象:
p_auto = new int(1024); // error: cannot assign a pointer to an auto_ptr
相反,必須調用 reset 函數來改變指針:
// revised test to guarantee p_auto refers to an object if (p_auto.get()) *p_auto = 1024; else // reset p_auto to a new object p_auto.reset(new int(1024));
調用 auto_ptr 對象的 reset 函數時,在將 auto_ptr 對象綁定到其他對象之前,會刪除 auto_ptr 對象所指向的對象(如果存在)。但是,正如自身賦值是沒有效果的一樣,如果調用該 auto_ptr 對象已經保存的同一指針的 reset 函數,也沒有效果,不會刪除對象。
auto_ptr 和內置指針對待復制和賦值有非常關鍵的重要區別。當復制 auto_ptr 對象或者將它的值賦給其他 auto_ptr 對象的時候,將基礎對象的所有權從原來的 auto_ptr 對象轉給副本,原來的 auto_ptr 對象重置為未綁定狀態。
在復制(或者賦值)auto_ptrs 對象之後,原來的 auto_ptr 對象不指向對象而新的 auto_ptr(左邊的 auto_ptr 對象)擁有基礎對象:
auto_ptr<string> ap1(new string("Stegosaurus")); // after the copy ap1 is unbound auto_ptr<string> ap2(ap1); // ownership transferred from ap1 to ap2
當復制 auto_ptr 對象或者對 auto_ptr 對象賦值的時候,右邊的auto_ptr 對象讓出對基礎對象的所有職責並重置為未綁定的 auto_ptr 對象之後,在上例中,刪除 string 對象的是 ap2 而不是 ap1,在復制之後,ap1 不再指向任何對象。
class test { public: test(int i = 0):index(i) {} ~ test() { printf("this is test[%d] destructor\n", index); } private: int index; }; int main() { auto_ptr<test> ptr1; cout << "ptr1.get() is " << ptr1.get() << endl; ptr1.reset(new test(1)); ptr1.reset(new test(0)); cout << "ptr1.get() is " << ptr1.get() << endl; auto_ptr<test> ptr2(ptr1); cout << "ptr1.get() is " << ptr1.get() << endl; cout << "ptr2.get() is " << ptr2.get() << endl; }
結果如下:
ptr1.get() is 0 this is test[1] destructor ptr1.get() is 0xd79030 ptr1.get() is 0 ptr2.get() is 0xd79030 this is test[0] destructor
除了將所有權從右操作數轉給左操作數之外,賦值還刪除左操作數原來指向的對象——假如兩個對象不同。通常自身賦值沒有效果。
auto_ptr<test> ptr1(new test(0)); auto_ptr<test> ptr2(new test(1)); cout << "ptr1.get() is " << ptr1.get() << endl; cout << "ptr2.get() is " << ptr2.get() << endl; ptr1 = ptr2; cout << "ptr1.get() is " << ptr1.get() << endl; cout << "ptr2.get() is " << ptr2.get() << endl;
結果是:
ptr1.get() is 0x2185010 ptr2.get() is 0x2185030 this is test[0] destructor ptr1.get() is 0x2185030 ptr2.get() is 0 this is test[1] destructor
將 ptr2 賦給 ptr1 之後:刪除了 ptr1 指向的對象;將 ptr1置為指向 ptr2 所指的對象;ptr2 是未綁定的 auto_ptr 對象。
註意:
a. 不要使用 auto_ptr 對象保存指向靜態分配對象的指針;
b. 永遠不要使用兩個 auto_ptrs 對象指向同一對象,導致這個錯誤的一種明顯方式是,使用同一指針來初始化或者 reset 兩個不同的 auto_ptr 對象。另一種導致這個錯誤的微妙方式可能是,使用一個 auto_ptr 對象的 get 函數的結果來初始化或者 reset另一個 auto_ptr 對象。
c. 不要使用 auto_ptr 對象保存指向動態分配數組的指針。當auto_ptr 對象被刪除的時候,它使用普通delete 操作符,而不用數組的 delete [] 操作符。
d. 不要將 auto_ptr 對象存儲在容器中。容器要求所保存的類型在復制(或者賦值)之後,兩個對象必須具有相同值,auto_ptr 類不滿足這個要求。
五:異常說明
1:查看普通函數聲明的時候,不可能確定該函數會拋出什麽異常,但是,為了編寫適當的 catch 子句,了解函數是否拋出異常以及會拋出哪種異常是很有用的。
異常說明指定,如果函數拋出異常,被拋出的異常將是包含在該說明中的一種,或者是從列出的異常中派生的類型。
異常說明跟在函數形參表之後。一個異常說明在關鍵字 throw 之後跟著一個(可能為空的)由圓括號括住的異常類型列表:
void recoup(int) throw(runtime_error);
空說明列表指出函數不拋出任何異常:
void no_problem() throw();
如果一個函數聲明沒有指定異常說明,則該函數可以拋出任意類型的異常。
異常說明是函數接口的一部分,函數定義以及該函數的任意聲明必須具有相同的異常說明。
如果函數拋出了沒有在其異常說明中列出的異常,就調用標準庫函數unexpected。默認情況下,unexpected 函數調用 terminate 函數,terminate 函數一般會終止程序。
像非成員函數一樣,成員函數聲明的異常說明跟在函數形參表之後。
class bad_alloc : public exception { public: ... virtual const char* what() const throw(); };
在 const 成員函數聲明中,異常說明跟在 const 限定符之後
基類中虛函數的異常說明,可以與派生類中對應虛函數的異常說明不同。但是,派生類虛函數的異常說明必須與對應基類虛函數的異常說明同樣嚴格,或者比後者更嚴格。所謂嚴格就是指,如果A會拋出a、b、c三種異常,而B會拋出a、b兩種異常,則B就比A嚴格。
這個限制保證,當使用指向基類類型的指針調用派生類虛函數的時候,派生類的異常說明不會增加新的可拋出異常。例如:
class Base { public: virtual double f1(double) throw (); virtual int f2(int) throw (std::logic_error); virtual std::string f3() throw (std::logic_error, std::runtime_error); }; class Derived : public Base { public: // error: exception specification is less restrictive than Base::f1‘s double f1(double) throw (std::underflow_error); // ok: same exception specification as Base::f2 int f2(int) throw (std::logic_error); // ok: Derived f3 is more restrictive std::string f3() throw (); };
派生類中 f1 的聲明是錯誤的,因為它的異常說明在基類 f1 版本列出的異常中增加了一個異常。派生類不能在異常說明列表中增加異常,通過派生類拋出的異常限制為由基類所列出的那些,在編寫代碼時就可以知道必須處理哪些異常。
異常說明是函數類型的一部分。這樣,也可以在函數指針的定義中提供異常說明:
void (*pf)(int) throw(runtime_error);
在用另一指針初始化帶異常說明的函數的指針,或者將後者賦值給函數地址的時候,兩個指針的異常說明不必相同,但是,源指針的異常說明必須至少與目標指針的一樣嚴格。
void recoup(int) throw(runtime_error); // ok: recoup is as restrictive as pf1 void (*pf1)(int) throw(runtime_error) = recoup; // ok: recoup is more restrictive than pf2 void (*pf2)(int) throw(runtime_error, logic_error) = recoup; // error: recoup is less restrictive than pf3 void (*pf3)(int) throw() = recoup; // ok: recoup is more restrictive than pf4 void (*pf4)(int) = recoup;
第三個初始化是錯誤的。指針聲明指出,pf3 指向不拋出任何異常的函數,但是recoup 函數指出它能拋出 runtime_error 類型的異常,recoup 函數拋出的異常類型超出了 pf3 所指定的,對 pf3 而言,recoup 函數不是有效的初始化式,並且會引發一個編譯時錯誤。
C++異常相關