C++異常處理:try,catch,throw,finally的用法
很多window系統有C-like介面,使用象like createWindow 和 destroyWindow函式來獲取和釋放window資源.
如果在w對應的window中顯示資訊時,一個異常被丟擲,w所對應的window將被丟失,就象其它動態分配的資源一樣.
解決方法與前面所述的一樣,建立一個類,讓它的建構函式與解構函式來獲取和釋放資源:
//一個類,獲取和釋放一個window 控制代碼 class WindowHandle { public: WindowHandle(WINDOW_HANDLE handle): w(handle) {} ~WindowHandle() { destroyWindow(w); }operator WINDOW_HANDLE() { return w; } // see below private: WINDOW_HANDLE w; // 下面的函式被宣告為私有,防止建立多個WINDOW_HANDLE拷貝 //有關一個更靈活的方法的討論請參見下面的靈巧指標 WindowHandle(const WindowHandle&); WindowHandle& operator=(const WindowHandle&); };
這看上去有些象auto_ptr,只是賦值操作與拷貝構造被顯式地禁止(參見More effective C++條款27),有一個隱含的轉換操作能把WindowHandle轉換為WINDOW_HANDLE.
這個能力對於使用WindowHandle物件非常重要,因為這意味著你能在任何地方象使用raw WINDOW_HANDLE一樣來使用WindowHandle.
(參見More effective C++條款5 ,瞭解為什麼你應該謹慎使用隱式型別轉換操作)
通過給出的WindowHandle類,我們能夠重寫displayInfo函式,如下所示:
// 如果一個異常被丟擲,這個函式能避免資源洩漏 void displayInfo(const Information& info) { WindowHandle w(createWindow()); //在w對應的window中顯式資訊;}
即使一個異常在displayInfo內被丟擲,被createWindow 建立的window也能被釋放.
資源應該被封裝在一個物件裡,遵循這個規則,你通常就能避免在存在異常環境裡發生資源洩漏.
但是如果你正在分配資源時一個異常被丟擲,會發生什麼情況呢?
例如當你正處於resource-acquiring類的建構函式中.
還有如果這樣的資源正在被釋放時,一個異常被丟擲,又會發生什麼情況呢?
建構函式和解構函式需要特殊的技術.
你能在More effective C++條款10和More effective C++條款11中獲取有關的知識.
丟擲一個異常的行為
個人認為接下來的這部分其實說的很經典,對我們理解異常行為/異常拷貝是很有幫助的.
條款12:理解“丟擲一個異常”與“傳遞一個引數”或“呼叫一個虛擬函式”間的差異
從語法上看,在函式裡宣告引數與在catch子句中宣告引數幾乎沒有什麼差別:
class Widget { ... }; //一個類,具體是什麼類在這裡並不重要 void f1(Widget w); // 一些函式,其引數分別為 void f2(Widget& w); // Widget, Widget&,或 void f3(const Widget& w); // Widget* 型別 void f4(Widget *pw); void f5(const Widget *pw); catch(Widget w) ... //一些catch 子句,用來 catch(Widget& w) ... //捕獲異常,異常的型別為 catch(const Widget& w) ... // Widget, Widget&, 或 catch(Widget *pw) ... // Widget* catch(const Widget *pw) ...
你因此可能會認為用throw丟擲一個異常到catch子句中與通過函式呼叫傳遞一個引數兩者基本相同.
這裡面確有一些相同點,但是他們也存在著巨大的差異.
讓我們先從相同點談起.
你傳遞函式引數與異常的途徑可以是傳值、傳遞引用或傳遞指標,這是相同的.
但是當你傳遞引數和異常時,系統所要完成的操作過程則是完全不同的.
產生這個差異的原因是:你呼叫函式時,程式的控制權最終還會返回到函式的呼叫處,但是當你丟擲一個異常時,控制權永遠不會回到丟擲異常的地方。
有這樣一個函式,引數型別是Widget,並丟擲一個Widget型別的異常:
// 一個函式,從流中讀值到Widget中 istream operator>>(istream& s, Widget& w); void passAndThrowWidget() { Widget localWidget; cin >> localWidget; //傳遞localWidget到 operator>> throw localWidget; // 丟擲localWidget異常 }
當傳遞localWidget到函式operator>>裡,不用進行拷貝操作,而是把operator>>內的引用型別變數w指向localWidget,任何對w的操作實際上都施加到localWidget上.
這與丟擲localWidget異常有很大不同.
不論通過傳值捕獲異常還是通過引用捕獲(不能通過指標捕獲這個異常,因為型別不匹配)都將進行lcalWidget的拷貝操作,也就說傳遞到catch子句中的是localWidget的拷貝.
必須這麼做,因為當localWidget離開了生存空間後,其解構函式將被呼叫.
如果把localWidget本身(而不是它的拷貝)傳遞給catch子句,這個子句接收到的只是一個被析構了的Widget,一個Widget的“屍體”.
這是無法使用的。因此C++規範要求被做為異常丟擲的物件必須被複制.
即使被丟擲的物件不會被釋放,也會進行拷貝操作.
例如如果passAndThrowWidget函式宣告localWidget為靜態變數(static):
void passAndThrowWidget() { static Widget localWidget; // 現在是靜態變數(static) 一直存在至程式結束 cin >> localWidget; // 象以前那樣執行 throw localWidget; // 仍將對localWidget進行拷貝操作 }
當丟擲異常時仍將複製出localWidget的一個拷貝.
這表示即使通過引用來捕獲異常,也不能在catch塊中修改localWidget;僅僅能修改localWidget的拷貝.
對異常物件進行強制複製拷貝,這個限制有助於我們理解引數傳遞與丟擲異常的第二個差異:丟擲異常執行速度比引數傳遞要慢.
當異常物件被拷貝時,拷貝操作是由物件的拷貝建構函式完成的.
該拷貝建構函式是物件的靜態型別(static type)所對應類的拷貝建構函式,而不是物件的動態型別(dynamic type)對應類的拷貝建構函式.
比如以下這經過少許修改的passAndThrowWidget:
class Widget { ... }; class SpecialWidget: public Widget { ... }; void passAndThrowWidget() { SpecialWidget localSpecialWidget; ... Widget& rw = localSpecialWidget; // rw 引用SpecialWidget throw rw; //它丟擲一個型別為Widget的異常 }
這裡丟擲的異常物件是Widget,即使rw引用的是一個SpecialWidget.
因為rw的靜態型別(static type)是Widget,而不是SpecialWidget.
你的編譯器根本沒有主要到rw引用的是一個SpecialWidget。編譯器所注意的是rw的靜態型別(static type).
這種行為可能與你所期待的不一樣,但是這與在其他情況下C++中拷貝建構函式的行為是一致的.
(不過有一種技術可以讓你根據物件的動態型別dynamic type進行拷貝,參見條款25)
異常是其它物件的拷貝,這個事實影響到你如何在catch塊中再丟擲一個異常.
比如下面這兩個catch塊,乍一看好像一樣:
catch(Widget& w) // 捕獲Widget異常 { ... // 處理異常 throw; // 重新丟擲異常,讓它 } // 繼續傳遞 catch(Widget& w) // 捕獲Widget異常 { ... // 處理異常 throw w; // 傳遞被捕獲異常的 } // 拷貝
這兩個catch塊的差別在於第一個catch塊中重新丟擲的是當前捕獲的異常,而第二個catch塊中重新丟擲的是當前捕獲異常的一個新的拷貝.
如果忽略生成額外拷貝的系統開銷,這兩種方法還有差異麼?
當然有。第一個塊中重新丟擲的是當前異常(current exception),無論它是什麼型別.
特別是如果這個異常開始就是做為SpecialWidget型別丟擲的,那麼第一個塊中傳遞出去的還是SpecialWidget異常,即使w的靜態型別(static type)是Widget.
這是因為重新丟擲異常時沒有進行拷貝操作.
第二個catch塊重新丟擲的是新異常,型別總是Widget,因為w的靜態型別(static type)是Widget.
一般來說,你應該用throw來重新丟擲當前的異常,因為這樣不會改變被傳遞出去的異常型別,而且更有效率,因為不用生成一個新拷貝.
(順便說一句,異常生成的拷貝是一個臨時物件.
正如條款19解釋的,臨時物件能讓編譯器優化它的生存期(optimize it out of existence),
不過我想你的編譯器很難這麼做,因為程式中很少發生異常,所以編譯器廠商不會在這方面花大量的精力)
讓我們測試一下下面這三種用來捕獲Widget異常的catch子句,異常是做為passAndThrowWidgetp丟擲的:
catch (Widget w) ... // 通過傳值捕獲異常 catch (Widget& w) ... // 通過傳遞引用捕獲異常 catch (const Widget& w) ... //通過傳遞指向const的引用捕獲異常
我們立刻注意到了傳遞引數與傳遞異常的另一個差異.
一個被異常丟擲的物件(剛才解釋過,總是一個臨時物件)可以通過普通的引用捕獲.
它不需要通過指向const物件的引用(reference-to-const)捕獲.
在函式呼叫中不允許轉遞一個臨時物件到一個非const引用型別的引數裡(參見條款19),但是在異常中卻被允許.
讓我們先不管這個差異,回到異常物件拷貝的測試上來.
我們知道當用傳值的方式傳遞函式的引數,我們製造了被傳遞物件的一個拷貝(參見Effective C++ 條款22),並把這個拷貝儲存到函式的引數裡.
同樣我們通過傳值的方式傳遞一個異常時,也是這麼做的。當我們這樣宣告一個catch子句時:
catch (Widget w) ... // 通過傳值捕獲
會建立兩個被丟擲物件的拷貝,一個是所有異常都必須建立的臨時物件,第二個是把臨時物件拷貝進w中.
同樣,當我們通過引用捕獲異常時:
catch (Widget& w) ... // 通過引用捕獲 catch (const Widget& w) ... file://也通過引用捕獲
這仍舊會建立一個被丟擲物件的拷貝:拷貝是一個臨時物件.
相反當我們通過引用傳遞函式引數時,沒有進行物件拷貝.
當丟擲一個異常時,系統構造的(以後會析構掉)被丟擲物件的拷貝數比以相同物件做為引數傳遞給函式時構造的拷貝數要多一個.
我們還沒有討論通過指標丟擲異常的情況,不過通過指標丟擲異常與通過指標傳遞引數是相同的.
不論哪種方法都是一個指標的拷貝被傳遞.
你不能認為丟擲的指標是一個指向區域性物件的指標,因為當異常離開區域性變數的生存空間時,該區域性變數已經被釋放.
Catch子句將獲得一個指向已經不存在的物件的指標。這種行為在設計時應該予以避免.
物件從函式的呼叫處傳遞到函式引數裡與從異常丟擲點傳遞到catch子句裡所採用的方法不同,
這只是引數傳遞與異常傳遞的區別的一個方面,第二個差異是在函式呼叫者或丟擲異常者與被呼叫者或異常捕獲者之間的型別匹配的過程不同.
比如在標準數學庫(the standard math library)中sqrt函式:
double sqrt(double); // from <cmath> or <math.h>
我們能這樣計算一個整數的平方根,如下所示:
int i; double sqrtOfi = sqrt(i);
毫無疑問,C++允許進行從int到double的隱式型別轉換,所以在sqrt的呼叫中,i 被悄悄地轉變為double型別,並且其返回值也是double.
(有關隱式型別轉換的詳細討論參見條款5)一般來說,catch子句匹配異常型別時不會進行這樣的轉換.
見下面的程式碼:
void f(int value) { try { if(someFunction()) // 如果 someFunction()返回 { throw value; //真,丟擲一個整形值 ... } } catch(double d) // 只處理double型別的異常 { ... } ... }
在try塊中丟擲的int異常不會被處理double異常的catch子句捕獲.
該子句只能捕獲真真正正為double型別的異常;不進行型別轉換.
因此如果要想捕獲int異常,必須使用帶有int或int&引數的catch子句.
不過在catch子句中進行異常匹配時可以進行兩種型別轉換.
第一種是繼承類與基類間的轉換.
一個用來捕獲基類的catch子句也可以處理派生類型別的異常.
例如在標準C++庫(STL)定義的異常類層次中的診斷部分(diagnostics portion )(參見Effective C++ 條款49).
捕獲runtime_errors異常的Catch子句可以捕獲range_error型別和overflow_error型別的異常,
可以接收根類exception異常的catch子句能捕獲其任意派生類異常.
這種派生類與基類(inheritance_based)間的異常型別轉換可以作用於數值、引用以及指標上:
catch (runtime_error) ... // can catch errors of type catch (runtime_error&) ... // runtime_error, catch (const runtime_error&) ... // range_error, or overflow_error catch (runtime_error*) ... // can catch errors of type catch (const runtime_error*) ... // runtime_error*,range_error*, oroverflow_error*
第二種是允許從一個型別化指標(typed pointer)轉變成無型別指標(untyped pointer),
所以帶有const void* 指標的catch子句能捕獲任何型別的指標型別異常:
catch (const void*) ... file://捕獲任何指標型別異常
傳遞引數和傳遞異常間最後一點差別是catch子句匹配順序總是取決於它們在程式中出現的順序.
因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有能處理該派生類異常的catch子句,與相同的try塊相對應.
例如:
try { ... } catch(logic_error& ex) // 這個catch塊 將捕獲 { ... // 所有的logic_error } // 異常, 包括它的派生類 catch(invalid_argument& ex) // 這個塊永遠不會被執行 { ... //因為所有的invalid_argument異常 都被上面的catch子句捕獲 }
與上面這種行為相反,當你呼叫一個虛擬函式時,被呼叫的函式位於與發出函式呼叫的物件的動態型別(dynamic type)最相近的類裡.
你可以這樣說虛擬函式採用最優適合法,而異常處理採用的是最先適合法.
如果一個處理派生類異常的catch子句位於處理基類異常的catch子句前面,編譯器會發出警告.
(因為這樣的程式碼在C++裡通常是不合法的)
不過你最好做好預先防範:不要把處理基類異常的catch子句放在處理派生類異常的catch子句的前面.
上面那個例子,應該這樣去寫:
try { ... } catch(invalid_argument& ex) // 處理 invalid_argument { ... } catch(logic_error& ex) // 處理所有其它的 { ... // logic_errors異常 }
綜上所述,把一個物件傳遞給函式或一個物件呼叫虛擬函式與把一個物件做為異常丟擲,這之間有三個主要區別.
第一、異常物件在傳遞時總被進行拷貝;當通過傳值方式捕獲時,異常物件被拷貝了兩次.
物件做為引數傳遞給函式時不需要被拷貝.
第二、物件做為異常被丟擲與做為引數傳遞給函式相比,前者型別轉換比後者要少(前者只有兩種轉換形式).