轉自:http://www.frontfree.net/view/article_755.html
原創:monkeyfu
處理在程序的運行時刻發生的錯誤,對于任何一個程序設計者來講都是不陌生的。對于錯誤的處理,我們有很多方法,本篇著重介紹的是C++中的錯誤異常處理。
在介紹C++中的錯誤異常處理之前,我們先來看一下常用的錯誤處理方式。
1.返回值 |
可以說這是最常用的錯誤處理方式之一,但其存在著一個致命的問題。就是返回值的檢查與否是由調用者主動控制的。如果調用者不檢查返回值,那也沒有任何
機制能夠強迫他這么做。再一個,考慮在C++中參數表相同而返回值不同的重載情況。在這種情況下,如果調用者不檢查返回值的話,編譯器根本不清楚應該調用哪個函數。 |
2.全局狀態標示符 |
這種辦法同返回值一樣,也是需要調用者主動檢查的。并且由于其是全局的,因此在多線程程序中,還必須保證它的線程安全性,必須要讓檢查者知道這是誰的返回值。 |
3.setjmp()/longjmp() |
你完全可以將longjmp()當成遠程的goto語句進行調用(goto語句只能左右于本地函數里)。但這個函數卻存在著很大甚至是致命的危險。暫且放下該函數會破壞結構化程序設計風格不說。其一,longjmp()只能處理int型的異常。其二,也就是最致命的一點就是,longjmp()不會調用析構函數,而C++的
異常處理機制卻會完成這個事情。因此,在C++中,千萬不要使用setjmp()、longjmp()函數。 |
4.斷言 |
對于斷言(Assert),其僅僅是在Debug版本中起作用,在Release中其是不存在的。另外斷言與我們通常所說的錯誤處理方式不同,他是用來處理我們可能會發生這個錯誤,并能夠避免的這種情況。 |
在介紹過上面那些存在問題的錯誤處理方式后,現在讓我們來看看C++中的異常機制是如何處理錯誤的。首先說,C++的異常處理不會像上面提到的那些方法一樣,必須是調用著主動檢查。因為在C++中,一旦拋出(throw)一個異常,而程序不捕獲(catch)的話,那么最終的結果就是abort()函數被調用,使得程序被終止。
下面我們來看一下C++異常處理(以下稱EH)的基本語法和語意。
其引入了3個關鍵字,分別是:
catch, throw, try
throw
異常由throw拋出,其格式為
函數在定義時通過異常規格申明定義其會拋出什么類型的異常,其格式為:
type-ID-list是一個可選項,其中包括了一個或多個類型的名字,它們之間以逗號分隔。
例如:
void func()
throw(int, some_class_type) |
則表明會拋出int和some_class_type類型異常。
對于一個空的異常規格申明,表示不拋出任何異常。
如:
而如果函數沒有異常規格申明,則表示會拋出任何類型的異常。
不過這里存在一種情況,例如:
void func()
throw(int) //指明拋出int型異常
{
...
subfunc(); //但可能從這里拋出非int型異常
...
} |
try -- catch
try塊中的異常處理函數對異常進行捕獲。其可以包含一個或多個處理函數,其形式如下:
catch (exception-declaration)
compound-statement |
處理函數的異常申明指明了其要捕獲什么類型的異常。
對于異常申明其可以是無名的,例如:catch(char
*),其表明會捕獲一個char *類型異常,但由于是無名的,因此不能對其進行操作。另外異常申明也可以存在如下形式:catch(...),其表明會捕獲任何類型的異常。
舉例:
void
func() throw(int,
some_class_type)
{
int
i;
........
throw
i;
........
}
int main()
{
try
{
func();
}
catch(int
e)
{
//處理int型異常
}
catch(some_class_type)
{
//處理some_class_type型異常
}
.......
return
0;
}
|
從上面的例子可以看出,當函數拋出異常時,throw后面要帶一個拋出的對象。但這并不是必須的,例如:
catch(int
e)
{ .......
throw;
} |
throw后面沒有接任何對象,這表明throw會再次拋出已存在的異常對象,因此其必須位于catch塊中。
下面介紹一些C++提供的標準異常
namespace
std
{
//exception派生
class logic_error;
//邏輯錯誤,在程序運行前可以檢測出來
//logic_error派生
class domain_error; //違反了前置條件
class invalid_argument; //指出函數的一個無效參數
class length_error; //指出有一個超過類型size_t的最大可表現值長度的對象的企圖
class out_of_range; //參數越界
class bad_cast; //在運行時類型識別中有一個無效的dynamic_cast表達式
class bad_typeid; //報告在表達試typeid(*p)中有一個空指針p
//exception派生
class runtime_error;
//運行時錯誤,僅在程序運行中檢測到
//runtime_error派生
class range_error; //違反后置條件
class overflow_error; //報告一個算術溢出
class bad_alloc; //存儲分配錯誤
}
|
在C++標準庫頭文件<exception>中申明了幾個EH類型和函數,它們是:
namespace
std
{
//EH類型
class bad_exception;
class exception;
typedef void (*terminate_handler)();
typedef void (*unexpected_handler)();
// 函數
terminate_handler set_terminate(terminate_handler) throw();
unexpected_handler set_unexpected(unexpected_handler) throw();
void terminate();
void unexpected();
bool uncaught_exception();
}
|
exception |
是所有標準庫拋出的異常的基類。 |
uncaught_exception() |
函數在異常被拋出卻沒有被捕獲時返回true,其它情況返回false |
terminate() |
在異常處理陷入了不可恢復狀態,如:重入時被調用。 |
unexpected() |
在函數拋出一個沒有在“異常規格申明”中申明的異常時被調用。 |
運行庫提供了缺省terminate_handler()和unexpected_handler()函數處理對應的情況。你可以通過set_terminate()和set_unexpected()函數替換庫的默認版本。這兩個函數,其可以獲取不帶輸入輸出參數的函數,并且該函數會返回原terminate或者unexpected函數的地址指針。以便在使用中調用或者以后的恢復。另外,在terminate
()中。其必須不返回或者拋出異常。
在介紹了EH的基本知識后讓我們來看看EH是如何工作的。
一般來說當發生函數調用的時候,都會進行諸如,保存寄存器值,參數壓棧,創建被調函數堆棧等保護現場的工作,而在函數返回的時候則會進行與此相反的恢復現場的工作。
這樣,當一個異常發生時,程序會在異常點處停止,然后開始搜索異常處理函數,其過程同函數返回相同,延調用棧向上搜索,直到找到一個與異常對象類型像匹配的異常申明,并進行相應的異常處理函數,在異常處理結束后,程序跳到異常處理函數所在try快最接近的下面一條語句開始執行。如果沒有找到合適的異常申明,則最終會調用std
:: unexpected(),并在其中調用std:terminate()直到abort(),程序被終止。
這也就意味著C++對于異常處理的模式始終是終止的。
例如:
#include
<iostream.h>
static void func(int n)
{
if (n)
throw 100;
}
extern int main()
{
try
{
func(1);
cout<<"程序不會執行到這里"<<endl;
}
catch(int)
{
cout<<"捕獲一個int型異常"<<endl;
}
catch(...)
{
cout<<"捕獲任意類型異常"<<endl;
}
cout<<"繼續執行"<<endl;
return 0;
}
|
該程序在運行時會打印如下信息:
捕獲一個int型異常
捕獲任意類型異常
至于異常處理的另一種模式恢復模式。可以通過循環檢測直到結果滿意為止。但在實際中,往往產生異常的地方與異常處理函數距離可能會比較遠,在這種情況下恢復模式就不那么可行了。
雖然,在異常處理延調用棧向上走的過程中回析構所有棧上的對象,但其并不會對堆中的對象進行處理,這樣將會引起嚴重的資源泄露問題。
例如:
void
func()
{
testclass *p = new testclass();
...
test(p); //這里會拋出異常
...
delete p;
//在拋出異常后,這里不會被執行,因此會導致內存泄露問題。
}
|
為了解決這個問題,C++提供了std::auto_ptr模板。其原理就是,將指針用一個棧上的模版實例保護起來,當發生異常的時候,模版會被析構,在析構函數中指針也就被delete了。
例如:
void func()
{ std::auto_ptr<testclass> p(new
testclass()); ... test(p.get());
...
} |
另外,在構造函數中拋出異常并不會引發析構函數。這一點要十分注意。因為這也會產生資源泄露問題。
例如:
class
test
{
public:
test() { c = new char[10]; throw -1;}
~test() {delete c;}
private:
char *c;
};
void proc()
{
try{
test t;
}
catch(int)
{
.......
}
}
|
由于異常是在test的構造函數中產生的,因此其不會引發其析構函數的調用。于是就如程序所示,產生了內存泄露問題。對于這種問題,最好的解決辦法還是使用auto_ptr。
對于析構函數,則不要在其中拋出異常。其原因在于析構函數會在其他異常拋出時被調用,這樣就會引發異常的重入問題,進而導致terminate()被調用。如果在析構函數中真要拋出異常,如:析構函數調用的函數會拋出異常等,則必須在該析構函數內將其捕獲。
前面說到要“找到一個與異常對象類型像匹配的異常申明”。事實上,這種匹配并不要求的十分準確。
考慮如下例子:
#include
<iostream.h>
class base
{
public:
virtual void what()
{
cout << "base" << endl;
}
};
class derived: public base
{
public:
void what()
{
cout << "derived" << endl;
}
};
void f()
{
throw derived();
}
main()
{
try
{
f();
}
catch(base b)
{
b.what();
}
try
{
f();
}
catch(base& b)
{
b.what();
}
}
|
其顯示結果為:
base
derived
為什么會這樣呢。因為如果異常拋出一個派生類對象,而恰好又其基類所捕獲到。那么該對象會被做"切片"處理。也就是說相對于基類,派生元素會被割下。在例子中derived的vptr會被設為base的virtual
table。因此虛函數what就會呈現出這種行為。而當通過引用捕獲時,得到的僅僅是其地址,對象不會被做切片處理。vptr因此也就不會發生變化,所以what仍然呈現出來derived的行為。
因此,這也就提醒我們將基類處理放在最后,在實際中更有意義。因為這樣可以盡可能的在前面的處理中保存信息。