寫在前面
所謂異常處理,即讓一個程序運行時遇到自己無法處理的錯誤時拋出一個異常,希望調用者可以發現處理問題.
異常處理的基本思想是簡化程序的錯誤代碼,為程序鍵壯性提供一個標準檢測機制.
也許我們已經使用過異常,但是你習慣使用異常了嗎?
現在很多軟件都是n*365*24小時運行,軟件的健壯性至關重要.
內容導讀
本文包括2個大的異常實現概念:C++的標準異常和SEH異常.
C++標準異常:
也許你很高興看到錯誤之后的Heap/Stack中對象被釋放,可是如果沒有呢?
又或者試想一下一個能解決的錯誤,需要我們把整個程序Kill掉嗎?
在《C++標準異?!分形蚁蚰阃扑]這幾章:
<使用異常規格編程> <構造和析構中的異常拋出> <使用析構函數防止資源泄漏>,以及深入一點的<拋出一個異常的行為>.
SEH異常:
我要問你你是一個WIN32程序員嗎?如果不是,那么也許你真的不需要看.
SEH是Windows的結構化異常,每一個WIN32程序員都應該要掌握它.
SEH功能強大,包括Termination handling和Exception handling兩大部分.
強有力的維護了代碼的健壯,雖然要以部分系統性能做犧牲(其實可以避免).
在SEH中有大量的代碼,已經在Win平臺上測試過了.
這里要提一下:在__finally處理中編譯器參與了絕大多數的工作,而Exception則是OS接管了幾乎所有的工作,也許我沒有提到的是:
對__finally來說當遇到ExitThread/ExitProcess/abort等函數時,finally塊不會被執行.
另:<使用析構函數防止資源泄漏>這個節點引用了More effective C++的條款9.
用2個列子,講述了我們一般都會犯下的錯誤,往往這種錯誤是我們沒有意識到的但確實是會給我們的軟件帶來致命的Leak/Crash,但這是有解決的方法的,那就是使用“靈巧指針”.
如果對照<More effective C++>的37條條款,關于異常的高級使用,有以下內容是沒有完成的:
1. 使用構造函數防止資源Leak(More effective C++ #10)
2. 禁止異常信息傳遞到析構Function外 (More effective C++ #11)
3. 通過引用捕獲異常 (More effective C++ #13)
4. 謹慎使用異常規格 (More effective C++ #14)
5. 了解異常處理造成的系統開銷 (More effective C++ #15)
6. 限制對象數量 (More effective C++ #26)
7. 靈巧指針 (More effective C++ #28)
C++異常 C++引入異常的原因
例如使用未經處理的pointer變的很危險,Memory/Resource Leak變的更有可能了.
寫出一個具有你希望的行為的構造函數和析構函數也變的困難(不可預測),當然最危險的也許是我們寫出的東東狗屁了,或者是速度變慢了.
大多數的程序員知道Howto use exception 來處理我們的代碼,可是很多人并不是很重視異常的處理(國外的很多Code倒是處理的很好,Java的Exception機制很不錯).
異常處理機制是解決某些問題的上佳辦法,但同時它也引入了許多隱藏的控制流程;有時候,要正確無誤的使用它并不容易.
在異常被throw后,沒有一個方法能夠做到使軟件的行為具有可預測性和可靠性
對C程序來說,使用Error Code就可以了,為什么還要引入異常?因為異常不能被忽略.
如果一個函數通過設置一個狀態變量或返回錯誤代碼來表示一個異常狀態,沒有辦法保證函數調用者將一定檢測變量或測試錯誤代碼.
結果程序會從它遇到的異常狀態繼續運行,異常沒有被捕獲,程序立即會終止執行.
在C程序中,我們可以用int setjmp( jmp_buf env );和 void longjmp( jmp_buf env, int value );
這2個函數來完成和異常處理相識的功能,但是MSDN中介紹了在C++中使用longjmp來調整stack時不能夠對局部的對象調用析構函數,
但是對C++程序來說,析構函數是重要的(我就一般都把對象的Delete放在析構函數中).
所以我們需要一個方法:
?、倌軌蛲ㄖ惓顟B,又不能忽略這個通知.
?、诓⑶襍earching the stack以便找到異常代碼時.
?、圻€要確保局部對象的析構函數被Call.
而C++的異常處理剛好就是來解決這些問題的.
有的地方只有用異常才能解決問題,比如說,在當前上下文環境中,無法捕捉或確定的錯誤類型,我們就得用一個異常拋出到更大的上下文環境當中去.
還有,異常處理的使用呢,可以使出錯處理程序與“通常”代碼分離開來,使代碼更簡潔更靈活.
另外就是程序必不可少的健壯性了,異常處理往往在其中扮演著重要的角色.
C++使用throw關鍵字來產生異常,try關鍵字用來檢測的程序塊,catch關鍵字用來填寫異常處理的代碼.
異??梢杂梢粋€確定類或派生類的對象產生。C++能釋放堆棧,并可清除堆棧中所有的對象.
C++的異常和pascal不同,是要程序員自己去實現的,編譯器不會做過多的動作.
throw異常類編程,拋出異常用throw, 如:
throw ExceptionClass(“my throw“);
例句中,ExceptionClass是一個類,它的構造函數以一個字符串做為參數.
也就是說,在throw的時候,C++的編譯器先構造一個ExceptionClass的對象,讓它作為throw的值拋出去,同時,程序返回,調用析構.
看下面這個程序:
#include <iostream.h>class ExceptionClass{ char* name;public: ExceptionClass(const char* name="default name") { cout<<"Construct "<<name<<endl; this->name=name; } ~ExceptionClass() { cout<<"Destruct "<<name<<endl; } void mythrow() { throw ExceptionClass("my throw"); }}void main(){ ExceptionClass e("Test"); try { e.mythrow(); } catch(...) { cout<<”*********”<<endl; }}
這是輸出信息:
Construct Test
Construct my throw
Destruct my throw
****************
Destruct my throw (這里是異常處理空間中對異常類的拷貝的析構)
Destruct Test
======================================
不過一般來說我們可能更習慣于把會產生異常的語句和要throw的異常類分成不同的類來寫,下面的代碼可以是我們更愿意書寫的.
class ExceptionClass{public: ExceptionClass(const char* name="Exception Default Class") { cout<<"Exception Class Construct String"<<endl; } ~ExceptionClass() { cout<<"Exception Class Destruct String"<<endl; } void ReportError() { cout<<"Exception Class:: This is Report Error Message"<<endl; }};class ArguClass{ char* name;public: ArguClass(char* name="default name") { cout<<"Construct String::"<<name<<endl; this->name=name; } ~ArguClass() { cout<<"Destruct String::"<<name<<endl; } void mythrow() { throw ExceptionClass("my throw"); }};_tmain(){ ArguClass e("haha"); try { e.mythrow(); } catch(int) { cout<<"If This is Message display screen, This is a Error!!"<<endl; } catch(ExceptionClass pTest) { pTest.ReportError(); } catch(...) { cout<<"***************"<<endl; }}
輸出Message:
Construct String::haha
Exception Class Construct String
Exception Class Destruct String
Exception Class:: This is Report Error Message
Exception Class Destruct String
Destruct String::haha
使用異常規格編程
如果我們調用別人的函數,里面有異常拋出,用去查看它的源代碼去看看都有什么異常拋出嗎?這樣就會很煩瑣.
比較好的解決辦法,是編寫帶有異常拋出的函數時,采用異常規格說明,使我們看到函數聲明就知道有哪些異常出現。
異常規格說明大體上為以下格式:
void ExceptionFunction(argument…)throw(ExceptionClass1, ExceptionClass2, ….)
所有異常類都在函數末尾的throw()的括號中得以說明了,這樣,對于函數調用者來說,是一清二楚的。
注意下面一種形式:
void ExceptionFunction(argument…) throw()
表明沒有任何異常拋出.
而正常的void ExceptionFunction(argument…)則表示:可能拋出任何一種異常,當然,也可能沒有異常,意義是最廣泛的.
異常捕獲之后,可以再次拋出,就用一個不帶任何參數的throw語句就可以了.
構造和析構中的異常拋出
這是異常處理中最要注意的地方了
先看個程序,假如我在構造函數的地方拋出異常,這個類的析構會被調用嗎?可如果不調用,那類里的東西豈不是不能被釋放了?
#include <iostream.h>#include <stdlib.h>class ExceptionClass1{ char* s;public: ExceptionClass1() { cout<<"ExceptionClass1()"<<endl; s=new char[4]; cout<<"throw a exception"<<endl; throw 18; } ~ExceptionClass1() { cout<<"~ExceptionClass1()"<<endl; delete[] s; }};void main(){ try { ExceptionClass1 e; } catch(...) {}}
結果為:
ExceptionClass1()
throw a exception
在這兩句輸出之間,我們已經給S分配了內存,但內存沒有被釋放(因為它是在析構函數中釋放的).
應該說這符合實際現象,因為對象沒有完整構造.
為了避免這種情況,我想你也許會說:應避免對象通過本身的構造函數涉及到異常拋出.
即:既不在構造函數中出現異常拋出,也不應在構造函數調用的一切東西中出現異常拋出.
但是在C++中可以在構造函數中拋出異常,經典的解決方案是使用STL的標準類auto_ptr.
其實我們也可以這樣做來實現:
在類中增加一個 Init()以及 UnInit();成員函數用于進行容易產生錯誤的資源分配工作,而真正的構造函數中先將所有成員置為NULL,然后調用 Init();
并判斷其返回值/或者捕捉 Init()拋出的異常,如果Init();失敗了,則在構造函數中調用 UnInit(); 并設置一個標志位表明構造失敗.
UnInit()中按照成員是否為NULL進行資源的釋放工作.
那么,在析構函數中的情況呢?
我們已經知道,異常拋出之后,就要調用本身的析構函數,如果這析構函數中還有異常拋出的話,則已存在的異常尚未被捕獲,會導致異常捕捉不到.
標準C++異常類
C++有自己的標準的異常類.
① 一個基類:
exception 是所有C++異常的基類.
class exception {public: exception() throw(); exception(const exception& rhs) throw(); exception& operator=(const exception& rhs) throw(); virtual ~exception() throw(); virtual const char *what() const throw();};
② 下面派生了兩個異常類:
logic_erro 報告程序的邏輯錯誤,可在程序執行前被檢測到.
runtime_erro 報告程序運行時的錯誤,只有在運行的時候才能檢測到.
以上兩個又分別有自己的派生類:
③ 由logic_erro派生的異常類
domain_error 報告違反了前置條件
invalid_argument 指出函數的一個無效參數
length_error 指出有一個產生超過NPOS長度的對象的企圖(NPOS為size_t的最大可表現值
out_of_range 報告參數越界
bad_cast 在運行時類型識別中有一個無效的dynamic_cast表達式
bad_typeid 報告在表達式typeid(*p)中有一個空指針P
④ 由runtime_error派生的異常
range_error 報告違反了后置條件
overflow_error 報告一個算術溢出
bad_alloc 報告一個存儲分配錯誤
使用析構函數防止資源泄漏
這部分是一個經典和很平常就會遇到的實際情況,下面的內容大部分都是從More Effective C++條款中得到的.
假設,你正在為一個小動物收容所編寫軟件,小動物收容所是一個幫助小狗小貓尋找主人的組織.
每天收容所建立一個文件,包含當天它所管理的收容動物的資料信息,你的工作是寫一個程序讀出這些文件然后對每個收容動物進行適當的處理(appropriate processing).
完成這個程序一個合理的方法是定義一個抽象類,ALA("Adorable Little Animal"),然后為小狗和小貓建立派生類.
一個虛擬函數processAdoption分別對各個種類的動物進行處理:
class ALA {public: virtual void processAdoption() = 0; ...};class Puppy: public ALA {public: virtual void processAdoption(); ...};class Kitten: public ALA {public: virtual void processAdoption(); ...};
你需要一個函數從文件中讀信息,然后根據文件中的信息產生一個puppy(小狗)對象或者kitten(小貓)對象.
這個工作非常適合于虛擬構造器(virtual constructor),在條款25詳細描述了這種函數.
為了完成我們的目標,我們這樣聲明函數:
// 從s中讀動物信息, 然后返回一個指針// 指向新建立的某種類型對象ALA * readALA(istream& s);
你的程序的關鍵部分就是這個函數,如下所示:
void processAdoptions(istream& dataSource){ while(dataSource) { ALA *pa = readALA(dataSource); //得到下一個動物 pa->processAdoption(); //處理收容動物 delete pa; //刪除readALA返回的對象 } }
這個函數循環遍歷dataSource內的信息,處理它所遇到的每個項目.
唯一要記住的一點是在每次循環結尾處刪除ps.
這是必須的,因為每次調用readALA都建立一個堆對象.如果不刪除對象,循環將產生資源泄漏。
現在考慮一下,如果pa->processAdoption拋出了一個異常,將會發生什么?
processAdoptions沒有捕獲異常,所以異常將傳遞給processAdoptions的調用者.
轉遞中,processAdoptions函數中的調用pa->processAdoption語句后的所有語句都被跳過,這就是說pa沒有被刪除.
結果,任何時候pa->processAdoption拋出一個異常都會導致processAdoptions內存泄漏.
很容易堵塞泄漏.
void processAdoptions(istream& dataSource){ while(dataSource) { ALA *pa = readALA(dataSource); try { pa->processAdoption(); } catch(...) { // 捕獲所有異常 delete pa; // 避免內存泄漏 // 當異常拋出時 throw; // 傳送異常給調用者 } delete pa; // 避免資源泄漏 } // 當沒有異常拋出時}
但是你必須用try和catch對你的代碼進行小改動.
更重要的是你必須寫雙份清除代碼,一個為正常的運行準備,一個為異常發生時準備.
在這種情況下,必須寫兩個delete代碼.
象其它重復代碼一樣,這種代碼寫起來令人心煩又難于維護,而且它看上去好像存在著問題.
不論我們是讓processAdoptions正常返回還是拋出異常,我們都需要刪除pa,所以為什么我們必須要在多個地方編寫刪除代碼呢?
我們可以把總被執行的清除代碼放入processAdoptions函數內的局部對象的析構函數里,這樣可以避免重復書寫清除代碼.
因為當函數返回時局部對象總是被釋放,無論函數是如何退出的.
(僅有一種例外就是當你調用longjmp時。Longjmp的這個缺點是C++率先支持異常處理的主要原因)
具體方法是用一個對象代替指針pa,這個對象的行為與指針相似。當pointer-like(類指針)對象被釋放時,我們能讓它的析構函數調用delete.
替代指針的對象被稱為smart pointers(靈巧指針),下面有解釋,你能使得pointer-like對象非常靈巧.
在這里,我們用不著這么聰明的指針,我們只需要一個pointer-lik對象,當它離開生存空間時知道刪除它指向的對象.
寫出這樣一個類并不困難,但是我們不需要自己去寫。標準C++庫函數包含一個類模板,叫做auto_ptr,這正是我們想要的.
每一個auto_ptr類的構造函數里,讓一個指針指向一個堆對象(heap object),并且在它的析構函數里刪除這個對象.
下面所示的是auto_ptr類的一些重要的部分:
template<class T>class auto_ptr{public: auto_ptr(T *p = 0): ptr(p) {} // 保存ptr,指向對象 ~auto_ptr() { delete ptr; } // 刪除ptr指向的對象private: T *ptr; // raw ptr to object};
auto_ptr類的完整代碼是非常有趣的,上述簡化的代碼實現不能在實際中應用.
(我們至少必須加上拷貝構造函數,賦值operator以及下面將要講到的pointer-emulating函數)
但是它背后所蘊含的原理應該是清楚的:用auto_ptr對象代替raw指針,你將不再為堆對象不能被刪除而擔心,即使在拋出異常時,對象也能被及時刪除.
(因為auto_ptr的析構函數使用的是單對象形式的delete,所以auto_ptr不能用于指向對象數組的指針.
如果想讓auto_ptr類似于一個數組模板,你必須自己寫一個。在這種情況下,用vector代替array可能更好)
auto_ptrtemplate<class T>class auto_ptr{public: typedef T element_type; explicit auto_ptr(T *p = 0) throw(); auto_ptr(const auto_ptr<T>& rhs) throw(); auto_ptr<T>& operator=(auto_ptr<T>& rhs) throw(); ~auto_ptr(); T& operator*() const throw(); T *operator->() const throw(); T *get() const throw(); T *release() const throw();};
使用auto_ptr對象代替raw指針,processAdoptions如下所示:
void processAdoptions(istream& dataSource){ while(dataSource) { auto_ptr<ALA> pa(readALA(dataSource)); pa->processAdoption(); }}
這個版本的processAdoptions在兩個方面區別于原來的processAdoptions函數.
第一, pa被聲明為一個auto_ptr<ALA>對象,而不是一個raw ALA*指針.
第二, 在循環的結尾沒有delete語句.
其余部分都一樣,因為除了析構的方式,auto_ptr對象的行為就象一個普通的指針。是不是很容易.
隱藏在auto_ptr后的思想是:
用一個對象存儲需要被自動釋放的資源,然后依靠對象的析構函數來釋放資源,這種思想不只是可以運用在指針上,還能用在其它資源的分配和釋放上.
想一下這樣一個在GUI程序中的函數,它需要建立一個window來顯式一些信息:
// 這個函數會發生資源泄漏,如果一個異常拋出
void displayInfo(const Information& info){ WINDOW_HANDLE w(createWindow());//在w對應的window中顯式信息 destroyWindow(w); }
很多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 belowprivate: 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異常}
綜上所述,把一個對象傳遞給函數或一個對象調用虛擬函數與把一個對象做為異常拋出,這之間有三個主要區別.
第一、異常對象在傳遞時總被進行拷貝;當通過傳值方式捕獲時,異常對象被拷貝了兩次.
對象做為參數傳遞給函數時不需要被拷貝.
第二、對象做為異常被拋出與做為參數傳遞給函數相比,前者類型轉換比后者要少(前者只有兩種轉換形式).
最后一點,catch子句進行異常類型匹配的順序是它們在源代碼中出現的順序,第一個類型匹配成功的catch將被用來執行.
當一個對象調用一個虛擬函數時,被選擇的函數位于與對象類型匹配最佳的類里,即使該類不是在源代碼的最前頭.
靈巧指針
第一次用到靈巧指針是在寫ADO代碼的時候,用到com_ptr_t靈巧指針;但一直印象不是很深;
其實靈巧指針的作用很大,對我們來說垃圾回收,ATL等都會使用到它.
在More effective 的條款后面特意增加這個節點,不僅是想介紹它在異常處理方面的作用,還希望對編寫別的類型代碼的時候可以有所幫助.
smart pointer(靈巧指針)其實并不是一個指針,其實是某種形式的類.
不過它的特長就是模仿C/C++中的指針,所以就叫pointer 了.
所以希望大家一定要記住兩點:smart pointer是一個類而非指針,但特長是模仿指針.
那怎么做到像指針的呢?
C++的模板技術和運算符重載給了很大的發揮空間.
首先smart pointer必須是高度類型化的(strongly typed ),模板給了這個功能.
其次需要模仿指針主要的兩個運算符->和*,那就需要進行運算符重載.
詳細的實現:
template<CLASS&NBSP; T> class SmartPtr{public: SmartPtr(T* p = 0); SmartPtr(const SmartPtr& p); ~SmartPtr(); SmartPtr& operator =(SmartPtr& p); T& operator*() const {return *the_p;} T* operator->() const {return the_p;}private: T *the_p;}
這只是一個大概的印象,很多東西是可以更改的.
比如可以去掉或加上一些const ,這都需要根據具體的應用環境而定.
注意重載運算符*和->,正是它們使smart pointer看起來跟普通的指針很相像.
而由于smart pointer是一個類,在構造函數、析構函數中都可以通過恰當的編程達到一些不錯的效果.
舉例:
比如C++標準庫里的std::auto_ptr 就是應用很廣的一個例子.
它的實現在不同版本的STL 中雖有不同,但原理都是一樣,大概是下面這個樣子:
template<CLASS&NBSP; X> class auto_ptr{public: typedef X element_type; explicit auto_ptr(X* p = 0) throw():the_p(p) {} auto_ptr(auto_ptr& a) throw():the_p(a.release()) {} auto_ptr& operator =(auto_ptr& rhs) throw() { reset(rhs.release()); return *this; } ~auto_ptr() throw() {delete the_p;} X& operator* () const throw() {return *the_p;} X* operator-> () const throw() {return the_p;} X* get() const throw() {return the_p;} X* release() throw() { X* tmp = the_p; the_p = 0; return tmp; } void reset(X* p = 0) throw() { if(the_p!=p) { delete the_p; the_p = p; } }private: X* the_p;};
關于auto_ptr 的使用可以找到很多的列子,這里不在舉了.
它的主要優點是不用 delete ,可以自動回收已經被分配的空間,由此可以避免資源泄露的問題.
很多Java 的擁護者經常不分黑白的污蔑C++沒有垃圾回收機制,其實不過是貽笑大方而已.
拋開在網上許許多多的商業化和非商業化的C++垃圾回收庫不提, auto_ptr 就足以有效地解決這一問題.
并且即使在產生異常的情況下, auto_ptr 也能正確地回收資源.
這對于寫出異常安全(exception-safe )的代碼具有重要的意義.
在使用smart pointer 的過程中,要注意的問題:
針對不同的smart pointer ,有不同的注意事項。比如auto_ptr ,就不能把它用在標準容器里,因為它只在內存中保留一份實例.
把握我前面說的兩個原則:smart pointer 是類而不是指針,是模仿指針,那么一切問題都好辦.
比如,smart pointer 作為一個類,那么以下的做法就可能有問題.
SmartPtr p;
if(p==0)
if(!p)
if(p)
很顯然, p 不是一個真正的指針,這么做可能出錯.
而SmartPtr 的設計也是很重要的因素.
您可以加上一個bool SmartPtr::null() const 來進行判斷.
如果堅持非要用上面的形式, 那也是可以的,我們就加上operator void* ()試試:
template<CLASS&NBSP; T> class SmartPtr{public: ... operator void*() const {return the_p;}... private: T* the_p;};
這種方法在basic_ios 中就使用過了。這里也可以更靈活地處理,比如類本身需要operator void*()這樣地操作,
那么上面這種方法就不靈了。但我們還有重載operator !()等等方法來實現.
總結smart pointer的實質:
smart pointer 的實質就是一個外殼,一層包裝。正是多了這層包裝,我們可以做出許多普通指針無法完成的事,比如前面資源自動回收,或者自動進行引用記數,比如ATL 中CComPtr 和 CComQIPtr 這兩個COM 接口指針類.
然而也會帶來一些副作用,正由于多了這些功能,又會使 smart pointer 喪失一些功能.
WIN結構化異常
對使用WIN32平臺的人來說,對WIN的結構化異常應該要有所了解的。WINDOWS的結構化異常是操作系統的一部分,而C++異常只是C++的一部分,當我們用C++編寫代碼的時候,我們選擇C++的標準異常(也可以用MS VC的異常),編譯器會自動的把我們的C++標準異常轉化成SEH異常。
微軟的Visual C++也支持C + +的異常處理,并且在內部實現上利用了已經引入到編譯程序和Windows操作系統的結構化異常處理的功能。
SEH實際包含兩個主要功能:結束處理(termination handling)和異常處理(exceptionhandling).
在MS VC的FAQ中有關于SEH的部分介紹,這里摘超其中的一句:
“在VC5中,增加了新的/EH編譯選項用于控制C++異常處理。C++同步異常處理(/EH)使得編譯器能生成更少的代碼,/EH也是VC的缺省模型。”
一定要記得在背后的事情:在使用SEH的時候,編譯程序和操作系統直接參與了程序代碼的執行。
Win32異常事件的理解
我寫的另一篇文章:內存處理和DLL技術也涉及到了SEH中的異常處理。
Exception(異常處理) 分成軟件和硬件exception2種.如:一個無效的參數或者被0除都會引起軟件exception,而訪問一個尚未commit的頁會引起硬件exception.
發生異常的時候,執行流程終止,同時控制權轉交給操作系統,OS會用上下文(CONTEXT)結構把當前的進程狀態保存下來,然后就開始search 一個能處理exception的組件,search order如下:
1. 首先檢查是否有一個調試程序與發生exception的進程聯系在一起,推算這個調試程序是否有能力處理
2. 如上面不能完成,操作系統就在發生exception event的線程中search exception event handler
3. search與進程關聯在一起的調試程序
4. 系統執行自己的exception event handler code and terminate process
結束處理程序
利用SEH,你可以完全不用考慮代碼里是不是有錯誤,這樣就把主要的工作同錯誤處理分離開來.
這樣的分離,可以使你集中精力處理眼前的工作,而將可能發生的錯誤放在后面處理.
微軟在Windows中引入SEH的主要動機是為了便于操作系統本身的開發.
操作系統的開發人員使用SEH,使得系統更加強壯.我們也可以使用SEH,使我們的自己的程序更加強壯.
使用SEH所造成的負擔主要由編譯程序來承擔,而不是由操作系統承擔.
當異常塊(exception block)出現時,編譯程序要生成特殊的代碼.
編譯程序必須產生一些表(table)來支持處理SEH的數據結構.
編譯程序還必須提供回調(callback)函數,操作系統可以調用這些函數,保證異常塊被處理.
編譯程序還要負責準備棧結構和其他內部信息,供操作系統使用和參考.
在編譯程序中增加SEH支持不是一件容易的事.
不同的編譯程序廠商會以不同的方式實現SEH,這一點并不讓人感到奇怪.
幸虧我們可以不必考慮編譯程序的實現細節,而只使用編譯程序的SEH功能.
(其實大多數編譯程序廠商都采用微軟建議的語法)
結束處理程序代碼初步
一個結束處理程序能夠確保去調用和執行一個代碼塊(結束處理程序,termination handler),
而不管另外一段代碼(保護體, guarded body)是如何退出的。結束處理程序的語法結構如下:
__try{file://保護塊}__finally{file://結束處理程序}
在上面的代碼段中,操作系統和編譯程序共同來確保結束處理程序中的__f i n a l l y代碼塊能夠被執行,不管保護體(t r y塊)是如何退出的。不論你在保護體中使用r e t u r n,還是g o t o,或者是longjump,結束處理程序(f i n a l l y塊)都將被調用。
=====================
************************
我們來看一個實列:(返回值:10, 沒有Leak,性能消耗:小)
DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData;}
這段代碼應該只是做為一個基礎函數,我們將在后面修改它,來看看結束處理程序的作用.
在代碼加一句:(返回值:5, 沒有Leak,性能消耗:中下)
DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5; return dwReturnData;}
在try塊的末尾增加了一個return語句.
這個return語句告訴編譯程序在這里要退出這個函數并返回dwTemp變量的內容,現在這個變量的值是5.
但是,如果這個return語句被執行,該線程將不會釋放信標,其他線程也就不能再獲得對信標的控制.
可以想象,這樣的執行次序會產生很大的問題,那些等待信標的線程可能永遠不會恢復執行.
通過使用結束處理程序,可以避免return語句的過早執行.
當return語句試圖退出try塊時,編譯程序要確保finally塊中的代碼首先被執行.
要保證finally塊中的代碼在try塊中的return語句退出之前執行.
在程序中,將ReleaseSemaphore的調用放在結束處理程序塊中,保證信標總會被釋放.
這樣就不會造成一個線程一直占有信標,否則將意味著所有其他等待信標的線程永遠不會被分配CPU時間.
在finally塊中的代碼執行之后,函數實際上就返回.
任何出現在finally塊之下的代碼將不再執行,因為函數已在try塊中返回,所以這個函數的返回值是5,而不是10.
讀者可能要問編譯程序是如何保證在try塊可以退出之前執行finally塊的.
當編譯程序檢查源代碼時,它看到在try塊中有return語句.
這樣,編譯程序就生成代碼將返回值(本例中是5)保存在一個編譯程序建立的臨時變量中.
編譯程序然后再生成代碼來執行finally塊中包含的指令,這稱為局部展開.
更特殊的情況是,由于try塊中存在過早退出的代碼,從而產生局部展開,導致系統執行finally塊中的內容.
在finally塊中的指令執行之后,編譯程序臨時變量的值被取出并從函數中返回.
可以看到,要完成這些事情,編譯程序必須生成附加的代碼,系統要執行額外的工作.
在不同的CPU上,結束處理所需要的步驟也不同.
例如,在Alpha處理器上,必須執行幾百個甚至幾千個CPU指令來捕捉try塊中的過早返回并調用finally塊.
在編寫代碼時,就應該避免引起結束處理程序的try塊中的過早退出,因為程序的性能會受到影響.
后面,將討論__leave關鍵字,它有助于避免編寫引起局部展開的代碼.
設計異常處理的目的是用來捕捉異常的—不常發生的語法規則的異常情況(在我們的例子中,就是過早返回).
如果情況是正常的,明確地檢查這些情況,比起依賴操作系統和編譯程序的SEH功能來捕捉常見的事情要更有效.
注意當控制流自然地離開try塊并進入finally塊(就像在Funcenstein1中)時,進入finally塊的系統開銷是最小的.
在x86CPU上使用微軟的編譯程序,當執行離開try塊進入finally塊時,只有一個機器指令被執行,讀者可以在自己的程序中注意到這種系統開銷.
當編譯程序要生成額外的代碼,系統要執行額外的工作時系統開銷就很值得注意了.
========================
修改代碼:(返回值:5,沒有Leak,性能消耗:中)
DWORD Func_SEHTerminateHandle(){ DWORD dwReturnData = 0; HANDLE hSem = NULL; const char* lpSemName = "TermSem"; hSem = CreateSemaphore(NULL, 1, 1, lpSemName); __try { WaitForSingleObject(hSem,INFINITE); dwReturnData = 5; if(dwReturnData == 5) goto ReturnValue; return dwReturnData; } __finally { ReleaseSemaphore(hSem,1,NULL); CloseHandle(hSem); } dwReturnData += 5;ReturnValue: return dwReturnData;}
代碼中,當編譯程序看到try塊中的goto語句,它首先生成一個局部展開來執行finally塊中的內容.
這一次,在finally塊中的代碼執行之后,在ReturnValue標號之后的代碼將執行,因為在try塊和finally塊中都沒有返回發生.
這里的代碼使函數返回5,而且,由于中斷了從try塊到finally塊的自然流程,可能要蒙受很大的性能損失(取決于運行程序的CPU)
寫上面的代碼是初步的,現在來看結束處理程序在我們代碼里面的真正的價值:
看代碼:(信號燈被正常釋放,reserve的一頁內存沒有被Free,安全性:安全)
DWORD TermHappenSomeError(){ DWORD dwReturnValue = 9; DWORD dwMemorySize = 1024; char* lpAddress; lpAddress = (char*)VirtualAlloc(NULL, dwMemorySize, MEM_RESERVE, PAGE_READWRITE);}
finally塊的總結性說明
我們已經明確區分了強制執行finally塊的兩種情況:
從try塊進入finally塊的正??刂屏?
•局部展開:從try塊的過早退出(goto、longjump、continue、break、return等)強制控制轉移到finally塊.
第三種情況,全局展開(globalunwind),在發生的時候沒有明顯的標識,我們在本章前面Func_SEHTerminate函數中已經見到.在Func_SEHTerminate的try塊中,有一個對TermHappenSomeError函數的調用。TermHappenSomeError函數會引起一個內存訪問違規(memory access violation),一個全局展開會使Func_SEHTerminate函數的finally塊執行.
由于以上三種情況中某一種的結果而導致finally塊中的代碼開始執行。為了確定是哪一種情況引起finally塊執行,可以調用內部函數AbnormalTermination:這個內部函數只在finally塊中調用,返回一個Boolean值.指出與finally塊相結合的try塊是否過早退出。換句話說,如果控制流離開try塊并自然進入finally塊,AbnormalTermination將返回FALSE。如果控制流非正常退出try塊—通常由于goto、return、break或continue語句引起的局部展開,或由于內存訪問違規或其他異常引起的全局展開—對AbnormalTermination的調用將返回TRUE。沒有辦法區別finally塊的執行是由于全局展開還是由于局部展開.
但這通常不會成為問題,因為可以避免編寫執行局部展開的代碼.(注意內部函數是編譯程序識別的一種特殊函數。編譯程序為內部函數產生內聯(inline)代碼而不是生成調用函數的代碼。例如,memcpy是一個內部函數(如果指定/Oi編譯程序開關)。當編譯程序看到一個對memcpy的調用,它直接將memcpy的代碼插入調用memcpy的函數中,而不是生成一個對memcpy函數的調用。其作用是代碼的長度增加了,但執行速度加快了。
在繼續之前,回顧一下使用結束處理程序的理由:
•簡化錯誤處理,因所有的清理工作都在一個位置并且保證被執行。
•提高程序的可讀性。
•使代碼更容易維護。
•如果使用得當,具有最小的系統開銷。
異常處理程序
異常是我們不希望有的事件。在編寫程序的時候,程序員不會想去存取一個無效的內存地址或用0來除一個數值。不過,這樣的錯誤還是常常會發生的。CPU負責捕捉無效內存訪問和用0除一個數值這種錯誤,并相應引發一個異常作為對這些錯誤的反應。CPU引發的異常,就是所謂的硬件異常(hardwareexception)。在本章的后面,我們還會看到操作系統和應用程序也可以引發相應的異常,稱為軟件異常(softwareexception)。
當出現一個硬件或軟件異常時,操作系統向應用程序提供機會來考察是什么類型的異常被引發,并能夠讓應用程序自己來處理異常。下面就是異常處理程序的語法:
__try{ //保護塊}__except(異常過慮器){ //異常處理程序}
注意__ e x c e p t關鍵字。每當你建立一個t r y塊,它必須跟隨一個f i n a l l y塊或一個e x c e p t塊。一個try 塊之后不能既有f i n a l l y塊又有e x c e p t塊。但可以在t r y - e x c e p t塊中嵌套t r y - f i n a l l y塊,反過來也可以。
異常處理程序代碼初步
與結束處理程序不同,異常過濾器( exception filter)和異常處理程序是通過操作系統直接執行的,編譯程序在計算異常過濾器表達式和執行異常處理程序方面不做什么事。下面幾節的內容舉例說明t r y - e x c e p t塊的正常執行,解釋操作系統如何以及為什么計算異常過濾器,并給出操作系統執行異常處理程序中代碼的環境。
本來想把代碼全部寫出來的,但是實在是寫這邊文擋化的時間太長了,所以接下來就只是做說明,而且try和except塊比較簡單。
盡管在結束處理程序的t r y塊中使用r e t u r n、g o t o、c o n t i n u e和b r e a k語句是被強烈地反對,但在異常處理程序的t r y塊中使用這些語句不會產生速度和代碼規模方面的不良影響。這樣的語句出現在與e x c e p t塊相結合的t r y塊中不會引起局部展開的系統開銷
當引發了異常時,系統將定位到e x c e p t塊的開頭,并計算異常過濾器表達式的值,過濾器表達式的結果值只能是下面三個標識符之一,這些標識符定義在windows的Except. h文件中。標識符定義為:
EXCEPTION_CONTINUE_EXECUTION(–1) // Exception is dismissed. Continue execution at the point where the exception occurred.
EXCEPTION_CONTINUE_SEARCH(0) // Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence.
EXCEPTION_EXECUTE_HANDLER(1) // Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution at the assembly instruction that was executing when the exception was raised
下面將討論這些標識符如何改變線程的執行。
下面的流程概括了系統如何處理一個異常的情況:(這里的流程假設是正向的)
*****開始 -> 執行一個CPU指令 -> {是否有異常被引發} -> 是 -> 系統確定最里層的try 塊 -> {這個try塊是否有一個except塊} -> 是 -> {過濾器表達式的值是什么} ->異常執行處理程序 -> 全局展開開始 -> 執行except塊中的代碼 -> 在except塊之后執行繼續*****
EXCEPTION_EXECUTE_HANDLER
在異常過濾器表達式的值如果是EXCEPTION_EXECUTE_HANDLER,這個值的意思是要告訴系統:“我認出了這個異常.
即,我感覺這個異??赡茉谀硞€時候發生,我已編寫了代碼來處理這個問題,現在我想執行這個代碼”
在這個時候,系統執行一個全局展開,然后執行向except塊中代碼(異常處理程序代碼)的跳轉.
在except塊中代碼執行完之后,系統考慮這個要被處理的異常并允許應用程序繼續執行。這種機制使windows應用程序可以抓住錯誤并處理錯誤,再使程序繼續運行,不需要用戶知道錯誤的發生。但是,當except塊執行后,代碼將從何處恢復執行?稍加思索,我們就可以想到幾種可能性:
第一種可能性是從產生異常的CPU指令之后恢復執行。這看起來像是合理的做法,但實際上,很多程序的編寫方式使得當前面的指令出錯時,后續的指令不能夠繼續成功地執行。代碼應該盡可能地結構化,這樣,在產生異常的指令之后的CPU指令有望獲得有效的返回值。例如,可能有一個指令分配內存,后面一系列指令要執行對該內存的操作。如果內存不能夠被分配,則所有后續的指令都將失敗,上面這個程序重復地產生異常。所幸的是,微軟沒有讓系統從產生異常的指令之后恢復指令的執行。這種決策使我們免于面對上面的問題。
第二種可能性是從產生異常的指令恢復執行。這是很有意思的可能性。如果在except塊中
有這樣的語句會怎么樣呢:在except塊中有了這個賦值語句,可以從產生異常的指令恢復執行。這一次,執行將繼續,不會產生其他的異常??梢宰鲂┬薷?,讓系統重新執行產生異常的指令。你會發現這種方法將導致某些微妙的行為。我們將在EXCEPTION_CONTINUE_EXECUTION一節中討論這種技術。
第三種可能性是從except塊之后的第一條指令開始恢復執行。這實際是當異常過濾器表達式的值為EXCEPTION_EXECUTE_HANDLER時所發生的事。在except塊中的代碼結束執行后,控制從except塊之后的第一條指令恢復。
c++異常參數傳遞
從語法上看,在函數里聲明參數與在catch子句中聲明參數是一樣的,catch里的參數可以是值類型,引用類型,指針類型.例如:
try{ .....}catch(A a){}catch(B& b){}catch(C* c){}
盡管表面是它們是一樣的,但是編譯器對二者的處理卻又很大的不同.
調用函數時,程序的控制權最終還會返回到函數的調用處,但是拋出一個異常時,控制權永遠不會回到拋出異常的地方.
class A;void func_throw(){ A a; throw a; //拋出的是a的拷貝,拷貝到一個臨時對象里}try{ func_throw();}catch(A a) //臨時對象的拷貝{}
當我們拋出一個異常對象時,拋出的是這個異常對象的拷貝。當異常對象被拷貝時,拷貝操作是由對象的拷貝構造函數完成的.
該拷貝構造函數是對象的靜態類型(static type)所對應類的拷貝構造函數,而不是對象的動態類型(dynamic type)對應類的拷貝構造函數。此時對象會丟失RTTI信息.
異常是其它對象的拷貝,這個事實影響到你如何在catch塊中再拋出一個異常。比如下面這兩個catch塊,乍一看好像一樣:
catch(A& w) // 捕獲異常{ // 處理異常 throw; // 重新拋出異常,讓它繼續傳遞}catch(A& w) // 捕獲Widget異常{ // 處理異常 throw w; // 傳遞被捕獲異常的拷貝}
第一個塊中重新拋出的是當前異常(current exception),無論它是什么類型。(有可能是A的派生類)
第二個catch塊重新拋出的是新異常,失去了原來的類型信息.
一般來說,你應該用throw來重新拋出當前的異常,因為這樣不會改變被傳遞出去的異常類型,而且更有效率,因為不用生成一個新拷貝.
看看以下這三種聲明:
catch (A w) ... // 通過傳值
catch (A& w) ... // 通過傳遞引用
catch (const A& w) ... //const引用
一個被異常拋出的對象(總是一個臨時對象)可以通過普通的引用捕獲;它不需要通過指向const對象的引用(reference-to-const)捕獲.
在函數調用中不允許轉遞一個臨時對象到一個非const引用類型的參數里,但是在異常中卻被允許.
回到異常對象拷貝上來,我們知道,當用傳值的方式傳遞函數的參數,我們制造了被傳遞對象的一個拷貝,并把這個拷貝存儲到函數的參數里.
同樣我們通過傳值的方式傳遞一個異常時,也是這么做的當我們這樣聲明一個catch子句時:
catch (A w) ... // 通過傳值捕獲
會建立兩個被拋出對象的拷貝,一個是所有異常都必須建立的臨時對象,第二個是把臨時對象拷貝進w中。實際上,編譯器會優化掉一個拷貝。同樣,當我們通過引用捕獲異常時,
catch (A& w) ... // 通過引用捕獲
catch (const A& w) ... //const引用捕獲
這仍舊會建立一個被拋出對象的拷貝:拷貝是一個臨時對象。相反當我們通過引用傳遞函數參數時,沒有進行對象拷貝.
話雖如此,但是不是所有編譯器都如此,VS200就表現很詭異.
通過指針拋出異常與通過指針傳遞參數是相同的.
不論哪種方法都是一個指針的拷貝被傳遞,你不能認為拋出的指針是一個指向局部對象的指針,因為當異常離開局部變量的生存空間時,該局部變量已經被釋放.
Catch子句將獲得一個指向已經不存在的對象的指針。這種行為在設計時應該予以避免.
另外一個重要的差異是在函數調用者或拋出異常者與被調用者或異常捕獲者之間的類型匹配的過程不同.
在函數傳遞參數時,如果參數不匹配,那么編譯器會嘗試一個類型轉換,如果存在的話。而對于異常處理的話,則完全不是這樣。見一下的例子:
void func_throw(){ CString a; throw a; //拋出的是a的拷貝,拷貝到一個臨時對象里}try{ func_throw();}catch(const char* s){ }
拋出的是CString,如果用const char*來捕獲的話,是捕獲不到這個異常的.
盡管如此,在catch子句中進行異常匹配時可以進行兩種類型轉換.第一種是基類與派生類的轉換,一個用來捕獲基類的catch子句也可以處理派生類類型的異常.
反過來,用來捕獲派生類的無法捕獲基類的異常.
第二種是允許從一個類型化指針(typed pointer)轉變成無類型指針(untyped pointer),所以帶有const void* 指針的catch子句能捕獲任何類型的指針類型異常:
catch (const void*) ... //可以捕獲所有指針異常
另外,你還可以用catch(...)來捕獲所有異常,注意是三個點.
傳遞參數和傳遞異常間最后一點差別是catch子句匹配順序總是取決于它們在程序中出現的順序.
因此一個派生類異??赡鼙惶幚砥浠惍惓5腸atch子句捕獲,這叫異常截獲,一般的編譯器會有警告.
class A{public: A() { cout << "class A creates" << endl; } void print() { cout << "A" << endl; } ~A() { cout << "class A destruct" << endl; }};class B: public A{public: B() { cout << "class B create" << endl; } void print() { cout << "B" << endl; } ~B() { cout << "class B destruct" << endl; }};void func(){ B b; throw b;}try{ func();}catch(B& b) //必須將B放前面,如果把A放前面,B放后面,那么B類型的異常會先被截獲。{ b.print();}catch(A& a){ a.print() ;}
相反的是,當你調用一個虛擬函數時,被調用的函數位于與發出函數調用的對象的動態類型(dynamic type)最相近的類里.
你可以這樣說虛擬函數匹配采用最優匹配法,而異常處理匹配采用的是最先匹配法.
附:
異常的描述
函數和函數可能拋出的異常集合作為函數聲明的一部分是有價值的,例如
void f(int a) throw(x2,x3);
表示f()只能拋出兩個異常x2,x3,以及這些類型派生的異常,但不會拋出其他異常.
如果f函數違反了這個規定,拋出了x2,x3之外的異常,例如x4,那么當函數f拋出x4異常時,
會轉換為一個std::unexpected()調用,默認是調用std::terminate(),通常是調用abort().
如果函數不帶異常描述,那么假定他可能拋出任何異常,例如:
int f();
//可能拋出任何異常
不帶任何異常的函數可以用空表表示:
int g() throw();
// 不會拋出任何異常
本文章部分內容參考hellodev的博客
新聞熱點
疑難解答