潛心研究C++異常處理機制數日,有所得,與大家共享:
C++異常處理機制核心觀點:
0.如果使用普通的處理方式:ASSERT,return等已經
足夠簡潔明了,請不要使用異常處理機制.
1.比C的setjump,longjump優秀.
2.可以處理任意類型的異常.
你可以人為地拋出任何類型的對象作為異常.
throw 100;
throw "hello";
...
3.需要一定的開銷,頻繁執行的關鍵代碼段避免使用
C++異常處理機制.
4.其強大的能力表現在:
A.把可能出現異常的代碼和異常處理代碼隔離開,結構更清晰.
B.把內層錯誤的處理直接轉移到適當的外層來處理,化簡了處理
流程.傳統的手段是通過一層層返回錯誤碼把錯誤處理轉移到
上層,上層再轉移到上上層,當層數過多時將需要非常多的判斷,
以采取適當的策略.
C.局部出現異常時,在執行處理代碼之前,會執行堆棧回退,即為
所有局部對象調用析構函數,保證局部對象行為良好.
D.可以在出現異常時保證不產生內存泄漏.通過適當的try,catch
布局,可以保證delete pobj;一定被執行.
E.在出現異常時,能夠獲取異常的信息,指出異常原因.
并可以給用戶優雅的提示.
F.可以在處理塊中嘗試錯誤恢復.保證程序幾乎不會崩潰.
通過適當處理,即使出現除0異常,內存訪問違例,也能
讓程序不崩潰,繼續運行,這種能力在某些情況下及其重要.
以上ABCDEF可以使你的程序更穩固,健壯,不過有時讓程序崩潰似乎更
容易找到原因,程序老是不崩潰,如果處理結果有問題,有時很難查找.
5.并不是只適合于處理'災難性的'事件.普通的錯誤處理也可以用異常機制
來處理,不過如果將此濫用的話,可能造成程序結構混亂,
因為異常處理機制本質上是程序處理流程的轉移,不恰當的,過度的轉移顯然
將造成混亂.許多人認為應該只在'災難性的'事件上使用異常處理,以避免異常
處理機制本身帶來的開銷,你可以認為這句話通常是對的.
6.先讓程序更脆弱,再讓程序更堅強.首先,它使程序非常脆弱,稍有差錯,馬上
執行流程跳轉掉,去尋找相應的處理代碼,以求適當的解決方式.
很像一個人身上帶著許多藥品,防護工具出行,稍有頭暈,馬上拿出清涼油;
遇到蚊子立刻拿出電蚊拍滅之.
WINDOWS:
7.將結構化異常處理結合/轉換到C++異常對象,可以更好地處理WINDOWS程序
出現的異常.
8.盡一切可能使用try,catch,而不是win32本身的結構化異常處理或者
MFC中的TRY,CATCH宏.
用得恰到好處,方顯C++異常之美妙!
1. 異常處理的使用
首先說明,千萬別對異常處理鉆牛角尖,那樣會死人的(當然是煩死的)!
在C++編程處理中,我秉承這樣一個思想,就是:能不用異常處理的就不用。因為造成的混亂實在是太——多了。如果能
用其他方法捕捉到錯誤并處理的話,誓死不用異常處理!呵呵,或許有點偏激,但我認為,這不失為一個避免不必要的錯誤的一個好辦法。當什么分配內存失敗,打
開文件失敗之類的通常錯誤,我們只需用assert,abort之類的函數就解決問題了。也就是說,假如有足夠的信息去處理一個錯誤,那么這個錯誤就不是
異常。
當然了,異常處理的存在也有它本身的意義和作用。不是你說不用就不用的,有些地方還非得用不可!
比如說,在當前上下文環境中,無法捕捉或確定的錯誤類型,我們就得用一個異常拋出到更大的上下文環境當中去。還有,異常處理的使用呢,可以使出錯處理程序與“通常”代碼分離開來,使代碼更簡潔更靈活。另外就是程序必不可少的健壯性了,異常處理往往在其中扮演著重要的角色。
OK,下面闡述一下。
2. 拋出異常
關——鍵字(周星馳的語氣):throw
例——句:throw ExceptionClass(“oh, shit! it’s a exception!L “);
例句中,ExceptionClass是一個類,它的構造函數以一個字符串做為參數,用來說明異常。也就是說,在throw的時候,C++的編譯器先構造一個ExceptionClass的對象,讓它作為throw的返回值,拋——出去。同時,程序返回,調用析構。看下面這個程序:
#include <iostream.h>
class ExceptionClass{
char* name;
public:
ExceptionClass(char* name="default name") {
cout<<"Construct "<<name<<endl;
this->name=name;
}
~ExceptionClass() {
cout<<"Destruct "<<name<<endl;
}
void mythrow(){
throw ExceptionClass("o,my god");
}
};
void main(){
ExceptionClass e("haha");
try {
e.mythrow();
} catch(...) {
}
}
大家看看結果就知道了,throw后,調用當前類的析構,整個結束了這個類的歷史使命。唉~~
3. 異常規格說明
如果我們調用別人的函數,里面有異常拋出,我用去查看它的源代碼去看看都有什么異常拋出嗎?可以,但是太——煩躁。比較好的解決辦法,是編寫帶有異常拋出的函數時,采用異常規格說明,使我們看到函數聲明就知道有哪些異常出現。
異常規格說明大體上為以下格式:
void ExceptionFunction(argument…) throw(ExceptionClass1, ExceptionClass2, ….)
對了,所有異常類都在函數末尾的throw()的括號中得以說明了,這樣,對于函數調用者來說,是一清二楚了!
注意下面一種形式:
void ExceptionFunction(argument…) throw()
表明沒有任何異常拋出。
而正常的void ExceptionFunction(argument…)則表示:可能拋出任何一種異常,當然就,也可能沒有異常,意義是最廣泛的哦。
4. 構造和析構中的異常拋出
55555,到了應該注意的地方了。
先看個程序,假如我在構造函數的地方拋出異常,這個類的析構會被調用嗎?可如果不調用,那類里的東西豈不是不能被釋放了??
程序:
#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分配了內存,哪里去了?內存釋放了嗎?沒有,沒有,因為它是在析構函數中釋放的,哇!問題大了去了。怎么辦?怎么辦?
為了避免這種情況,應避免對象通過本身的構造函數涉及到異常拋出。即:既不在構造函數中出現異常拋出,也不應在構造函數調用的一切東西中出現異常拋出。否則,只有完蛋。
那么,在析構函數中的情況呢?我們已經知道,異常拋出之后,就要調用本身的析構函數,如果這析構函數中還有異常拋出的話,則已存在的異常尚未被捕獲,會導致異常捕捉不到哩。
完,也就是說,我們不要在構造函數和析構函數中存在異常拋出。
5. 異常捕獲
上邊的程序不知道大家看懂了沒,異常捕獲已經在上面出現了也。
沒錯,就是try{…}catch(…){…}這樣的結構!
Try后面的花括號中,就是有可能涉及到異常的各種聲明啊調用啊之類的,如果有異常拋出,就會被異常處理器截獲捕捉到,轉給catch處理。先把異常的類和catch后面小括號中的類進行比較,如果一致,就轉到后面的花括號中進行處理。
例如拋出異常是這么寫的:
void f(){throw ExceptionClass(“ya, J”);}
假設類ExceptionClass有個成員函數function()在有異常時進行處理或相應的消息顯示(只是做個例子哦,別挑我的刺兒)。
那么,我可以這么捕捉: try{f()}catch(ExceptionClass e){e.function()};
當然,象在上面程序中出現的一樣,我可以在catch后用三個點來代表所有異常。如try{f()}catch(…){}。這樣就截斷了所有出現的異常。有助于把所有沒出現處理的異常屏蔽掉(我是這么認為的J)。
異常捕獲之后,我可以再次拋出,就用一個不帶任何參數的throw語句就可以了,例如:try(f())catch(…){throw}
6. 標準異常
正象許多人想象的一樣,C++肯定有自己的標準的異常類。
一個總基類:
exception 是所有C++異常的基類。
下面派生了兩個異常類:
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 報告一個存儲分配錯誤
呼呼,這是我這兩天研究異常的總結報告。呼呼,累。
C++編譯器如何實現異常處理1 -- 摘自互聯網
與傳統語言相比,
C++的一
項革命性創新就是它支持異常處理。傳統的錯誤處理方式經常滿足不了要求,而異常處理則是一個極好的替代解決方案。它將正常
代碼和錯誤處理代碼清晰的劃分開來,程序變得非常干凈并且容易維護。本文討論了編譯器如何實現異常處理。我將假定你已經熟悉異常處理的語法和機制。本文還
提供了一個用于V
C++的異常處理庫,要用庫中的處理程序替換掉VC++提供的那個,你只需要調用下面這個函數:
之后,程序中的所有異常,從它們被
拋出到堆棧展開(stack unwinding),再到調用catch塊,最后到程序恢復正常運行,都將由我的異常處理庫來管理。
與其它C++特性一樣,C++標準并沒有規定編譯器應該如何來實現異常處理。這意味著每一個編譯器的提供商都可以用它們認為恰當的方式來實現它。下面我
會描述一下VC++是怎么做的,但即使你使用其它的編譯器或操作系統①,本文也應該會是一篇很好的學習材料。V
C++的實現方式是以windows系統的 結構化異常處理(SEH)②為基礎的。
結構化異常處理—概述
在本文的討論中,我認為異常或者是被明確的
拋出的,
或者是
由于除零溢出、空指針訪問等引起的。當它發生時會產生一個中斷,接下來控制權就會傳遞到操作系統的手中。操作系統將調用異常處理程序,檢查從異常發生位置
開始的函數調用序列,進行堆棧展開和控制權轉移。Windows定義了結構“EXCEPTION_REGISTRATION”,使我們能夠向操作系統注冊
自己的異常處理程序。
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
}; |
注冊時,只需要創建這樣一個結構,然后把它的地址放到FS段偏移0的位置上去就行了。下面這句匯編代碼演示了這一操作:
mov FS:[0], exc_regp
prev字段用于建立一個EXCEPTION_REGISTRATION結構的鏈表,每次注冊新的EXCEPTION_REGISTRATION時,我們都要把原來注冊的那個的地址存到prev中。
那么,那個異常
回調函數長什么樣呢?在excpt.h中,windows定義了它的原形:
EXCEPTION_DISPOSITION (*handler)(
_EXCEPTION_RECORD *ExcRecord,
void* EstablisherFrame,
_CONTEXT *ContextRecord,
void* DispatcherContext); |
不要管它的參數和返回值,我們先來看一個簡單的例子。下面的程序注冊了
一個異常處理程序,然后通過除以零產生了
一個異常。異常處理程序捕獲了它,打印了一條消息就完事大吉并退出了。
#include <iostream>
#include <windows.h>
using std::cout;
using std::endl;
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
};
EXCEPTION_DISPOSITION myHandler(
_EXCEPTION_RECORD *ExcRecord,
void * EstablisherFrame,
_CONTEXT *ContextRecord,
void * DispatcherContext)
{
cout << "In the exception handler" << endl;
cout << "Just a demo. exiting..." << endl;
exit(0);
return ExceptionContinueExecution; //不會運行到這
}
int g_div = 0;
void bar()
{
//初始化一個EXCEPTION_REGISTRATION結構
EXCEPTION_REGISTRATION reg, *preg = ?
reg.handler = (DWORD)myHandler;
//取得當前異常處理鏈的“頭”
DWORD prev;
_asm
{
mov EAX, FS:[0]
mov prev, EAX
}
reg.prev = (EXCEPTION_REGISTRATION*) prev;
//注冊!
_asm
{
mov EAX, preg
mov FS:[0], EAX
}
//產生一個異常
int j = 10 / g_div; //異常,除零溢出
}
int main()
{
bar();
return 0;
}
/*-------輸出-------------------
In the exception handler
Just a demo. exiting...
---------------------------------*/ |
注意EXCEPTION_REGISTRATION必須定義在棧上,并且必須位于比上一個結點更低的內存地址上,Windows對此有嚴格要求,達不到的話,它就會立刻終止進程。
函數和堆棧
堆棧是用來保存局部對象的連續內存區。更明確的說,每個函數都有一個相關的棧楨(stack
frame)來保存它所有的局部對象和表達式計算過程中用到的臨時對象,至少理論上是這樣的。但現實中,編譯器經常會把一些對象放到寄存器中以便能以更快
的速度訪問。堆棧是一個處理器(CPU)層次的概念,為了操縱它,處理器提供了一些專用的寄存器和指令。
圖1是一個典型的堆棧,它示出了函數foo調用bar,bar又調用widget時的情景。請注意堆棧是向下增長的,這意味著新壓入的項的地址低于原有項的地址。
通常編譯器使用EBP寄存器來指示當前活動的棧楨。本例中,CPU正在運行widget,所以圖中的EBP指向了widget的棧楨。編譯器在編譯時將
所有局部對象解析成相對于棧楨指針(EBP)的固定偏移,函數則通過棧楨指針來間接訪問局部對象。舉個例子,典型的,widget訪問它的局部變量時就是
通過訪問棧楨指針以下的、有著確定位置的幾個字節來實現的,比如說EBP-24。
上圖中也畫出了ESP寄存器,它叫棧指針,指向棧的最后一項。在本例中,ESP指著widget的棧楨的末尾,這也是下一個棧楨(如果它被創建的話)的開始位置。
處理器支持兩種類型的棧操作:壓棧(push)和彈棧(pop)。比如,pop
EAX的作用是從ESP所指的位置讀出4字節放到EAX寄存器中,并把ESP加上(記住,棧是向下增長的)4(在32位處理器上);類似的,push
EBP的作用是把ESP減去4,然后將EBP的值放到ESP指向的位置中去。
編譯器編譯一個函數時,會在它的開頭添加一些代碼來為其創建并初始化棧楨,這些代碼被稱為序言(prologue);同樣,它也會在函數的結尾處放上代碼來清除棧楨,這些代碼叫做尾聲(epilogue)。
一般情況下,序言是這樣的:
Push EBP ; 把原來的棧楨指針保存到棧上
Mov EBP, ESP ; 激活新的棧楨
Sub ESP, 10 ; 減去一個數字,讓ESP指向棧楨的末尾 |
第一條指令把原來的棧楨指針EBP保存到棧上;第二條指令通過讓EBP指向主調函數的EBP的保存位置來激活被調函數的棧楨;第三條指令把ESP減去了
一個數字,這樣ESP就指向了當前棧楨的末尾,而這個數字是函數要用到的所有局部對象和臨時對象的大小。編譯時,編譯器知道函數的所有局部對象的類型和
“體積”,所以,它能很容易的計算出棧楨的大小。
尾聲所做的正好和序言相反,它必須把當前棧楨從棧上清除掉:
Mov ESP, EBP
Pop EBP ; 激活主調函數的棧楨
Ret ; 返回主調函數 |
它讓ESP指向主調函數的棧楨指針的保存位置(也就是被調函數的棧楨指針指向的位置),彈出EBP從而激活主調函數的棧楨,然后返回主調函數。
一旦CPU遇到返回指令,它就要做以下兩件事:把返回地址從棧中彈出,然后跳轉到那個地址去。返回地址是主調函數執行call指令調用被調函數時自動壓
棧的。Call指令執行時,會先把緊隨在它后面的那條指令的地址(被調函數的返回地址)壓入棧中,然后跳轉到被調函數的開始位置。圖2更詳細的描繪了運行
時的堆棧。如圖所示,主調函數把被調函數的參數也壓進了堆棧,所以參數也是棧楨的一部分。函數返回后,主調函數需要移除這些參數,它通過把所有參數的總體
積加到ESP上來達到目的,而這個體積可以在編譯時知道:
當然,也可以把參數的總體積寫在被調函數的返回指令的后面,讓被調函數去移除參數,下面的指令就在返回主調函數前從棧中移去了24個字節:
取決于被調函數的調用約定(call convention),這兩種方式每次只能用一個。你還要注意的是每個線程都有自己獨立的堆棧。
C++和異常
回憶一下我在第一節中介紹的EXCEPTION_REGISTRATION結構,我們曾用它向操作系統注冊了發生異常時要被調用的回調函數。VC++也是這么做的,不過它擴展了這個結構
的語義,在它的后面添加了兩個新字段:
struct EXCEPTION_REGISTRATION
{
EXCEPTION_REGISTRATION* prev;
DWORD handler;
int id;
DWORD ebp;
}; |
VC++會為絕大部分函數③添加一個EXCEPTION_REGISTRATION類型的局部變量,它的最后一個字段(ebp)與棧楨指針指向的位置重
疊。函數的序言創建這個結構并把它注冊給操作系統,尾聲則恢復主調函數的EXCEPTION_REGISTRATION。id字段的意義我將在下一節介
紹。
VC++編譯函數時會為它生成兩部分數據:
a)異常回調函數
b)一個包含函數重要信息的數據結構,這些信息包括catch塊、這些塊的地址和這些塊所關心的異常的類型等等。我把這個結構稱為funcinfo,有關它的詳細討論也在下一節。
圖3是考慮了異常處理之后的運行時堆棧。widget的異常回調函數位于由FS:[0]指向的異常處理鏈的開始位置(這是由widget的序言設置
的)。異常處理程序把widget的funcinfo結構的地址交給函數__CxxFrameHandler,__CxxFrameHandler會檢查
這個結構看函數中有沒有catch塊對當前的異常感興趣。如果沒有的話,它就返回ExceptionContinueSearch給操作系統,于是操作系
統會從異常處理鏈表中取得下一個結點,并調用它的異常處理程序(也就是調用當前函數的那個函數的異常處理程序)。
這一過程將一直進行下去——直到處理程序找到一個能處理當前異常的catch塊為止,這時它就不再返回操作系統了。但是在調用catch塊之前(由于有
funcinfo結構,所以知道catch塊的入口,參見圖3),必須進行堆棧展開,也就是清理掉當前函數的棧楨下面的所有其他的棧楨。這個操作稍微有點
復雜,因為:異常處理程序必須找到異常發生時生存在這些棧楨上的所有局部對象,并依次調用它們的析構函數。后面我將對此進行詳細介紹。
異常處理程序把這項工作委托給了各個棧楨自己的異常處理程序。從FS:[0]指向的異常處理鏈的第一個結點開始,它 依次調用每個結點的處理程序,告訴它堆棧正在展開。與之相呼應,這些處理程序會調用每個局部對象的析構函數,然后返回。此過程一直進行到與異常處理程序自 身相對應的那個結點為止。
由于catch塊是函數的一部分,所以它使用的也是函數的棧楨。因此,在調用catch塊之前,異常處理程序必須激活它所隸屬的函數的棧楨。
其次,每個catch塊都只接受一個參數,其類型是它希望捕獲的異常的類型。異常處理程序必須把異常對象本身或者是異常對象的引用拷貝到catch塊的棧
楨上,編譯器在funcinfo中記錄了相關信息,處理程序根據這些信息就能知道到哪去拷貝異常對象了。
拷貝完異常并激活棧楨后,處理程序將調用catch塊。而catch塊將把控制權下一步要轉移到的地址返回來。請注意:雖然這時堆棧已經展開,棧楨也都
被清除了,但它們占據的內存空間并沒有被覆蓋,所有的數據都還好好的待在棧上。這是因為異常處理程序仍在執行,象其他函數一樣,它也需要棧來存放自己的局
部對象,而其棧楨就位于發生異常的那個函數的棧楨的下面。catch塊返回以后,異常處理程序需要“殺掉”異常對象。此后,它讓ESP指向目標函數(控制
權要轉移到的那個函數)的棧楨的末尾——這樣就把(包括它自己的在內的)所有棧楨都刪除了,然后再跳轉到catch塊返回的那個地址去,就勝利的完成整個
異常處理任務了。但它怎么知道目標函數的棧楨末尾在哪呢?事實上它沒法知道,所以編譯器把這個地址保存到了棧楨上(由前言來完成),如圖3所示,棧楨指針
EBP下面第16個字節就是。
當然,catch塊也可能拋出新異常,或者是將原來的異常重新拋出。處理程序必須對此有所準備。如果是拋出新異常,它必須殺掉原來的那個;而如果是重新拋出原來的異常,它必須能繼續傳播(propagate)這個異常。
這里我要特別強調一點:由于每個線程有自己獨立的堆棧,所以每個線程也都有自己獨立的、由FS:[0]指向的EXCEPTION_REGISTRATION鏈。