C++异常处理机制详解
異常處理是一種允許兩個獨立開發的程序組件在程序執行期間遇到程序不正常的情況(異常exception)時相互通信的機制。本文總結了19個C++異常處理中的常見問題,基本涵蓋了一般C++程序開發所需的關于異常處理部分的細節。
?
1. throw可以拋出哪些種類的異常對象?如何捕獲?
1)異常對象通常是一個class對象, 通常用以下代碼拋出:
// 調用的類的構造函數
throw popOnEmpty();
但是throw 表達式也可以拋出任何類型的對象, 例如(雖然很不常見)在下面的代碼例子中,函數mathFunc()拋出一個枚舉類型的異常對象
enum EHstate { noErr, zeroOp, negativeOp, severeError };
int mathFunc( int i )
{
??? if ( i == 0 )
??? throw zeroOp; // 枚舉類型的異常
}
2)拋出異常的語句或其調用函數要在try塊中才能被捕獲。
?
2. catch子句的語法
一個catch 子句由三部分構成:
1)關鍵字catch
2)異常聲明,在括號中的單個類型或單個對象聲明被(稱作異常聲明,exception declaration)
3)復合語句中的一組語句。
// stackExcp.h
class popOnEmpty { };
class popOnFull { };
catch ( pushOnFull )
{
??? cerr << "trying to push a value on a full stack\n";
??? return errorCode88;
}
?
3. 異常聲明可以只是一個類型聲明而不是對象聲明嗎?
catch 子句的異常聲明可以是一個類型聲明或一個對象聲明。當我們要獲得throw 表達式的值或者要操縱throw 表達式所創建的異常對象時,我們應該聲明一個對象。
catch ( pushOnFull eObj )
{
??? cerr << "trying to push the value " << eObj.value() << " on a full stack\n";
}
?
4. 異常聲明中異常對象的拷貝過程?
catch 子句異常聲明的行為特別像參數聲明。同理,也可以分出按值傳遞和引用傳遞(指針)。通常采用的是引用傳遞。
例1:按值傳遞。當進入catch 子句時,如果異常聲明聲明了一個對象,則用該異常對象的拷貝初始化這個對象。例中對象eObj 是用異常對象的值來初始化的,會調用拷貝構造函數。
void calculate( int op ) {
try {
mathFunc( op );
}
catch (pushOnFull eObj ) {
// eObj 是被拋出的異常對象的拷貝
}
}
例2:引用傳遞。catch子句可以直接引用由throw 表達式創建的異常對象,而不是創建一個局部拷貝。可以防止不必要地拷貝大型類對象。
void calculate( int op ) {
try {
mathFunc( op );
}
catch (pushOnFull &eObj ) {
// eObj 引用了被拋出的異常對象
}
}
?
5. 異常處理的棧展開過程是什么?
在查找用來處理被拋出異常的catch 子句時,因為異常而退出復合語句和函數定義,這個過程被稱作棧展開(stack unwinding)。隨著棧的展開,在退出的復合語句和函數定義中聲明的局部變量的生命期也結束了。C++保證,隨著棧的展開,盡管局部類對象的生命期是因為拋出異常而被結束,但是這些局部類對象的析構函數也會被調用。
?
6. 異常拋出沒有在try塊中或拋出的異常沒有對應的catch語句來捕捉,結果如何?
異常不能夠保持在未被處理的狀態,異常對于一個程序非常重要,它表示程序不能夠繼續正常執行。如果沒有找到處理代碼,程序就調用C++標準庫中定義的函數terminate()。terminate()的缺省行為是調用abort() ,指示從程序非正常退出。
?
7.為什么要重新拋出異常?怎么寫?
在異常處理過程中也可能存在“單個catch 子句不能完全處理異常”的情況。在對異常對象進行修改或增加某些信息之后,catch 子句可能決定該異常必須由函數調用鏈中更上級的函數來處理。表達式的形式為:throw;
例子如下:
try
????? ??{
??????????? entryDescr->checkMandatoryData(beModel_);
??????? }
??????? catch (CatchableOAMexception & error) // 只能用引用聲明
??????? {
??????????? vector<string> paramList;
??????????? paramList.push_back(currentDn);
??????????? error.addFrameToEnd(6,paramList);? // 修改異常對象
??????????? throw;? //重新拋出異常, 并由另一個catch 子句來處理
??????? }
注意1:被重新拋出的異常就是原來的異常對象,所以異常聲明一定要用引用。
注意2:在catch 語句里也可以拋出其它
?
8. 怎么捕捉全部異常或未知異常?
可以用catch ( ... ) { } 。
作用在于:1. 可以釋放在前面獲得的資源(如動態內存),因為異常退出,這些資源為釋放。2. 捕獲其余類型的未知異常。
catch 子句被檢查的順序與它們在try 塊之后出現的順序相同。一旦找到了一個匹配,則后續的catch 子句將不再檢查。這意味著如果catch(...)與其他catch 子句聯合使用,它必須總是被放在異常處理代碼表的最后,否則就會產生一個編譯時刻錯誤。例子如下:
catch ( pushOnFull ) {}
catch ( popOnEmpty ) { }
catch (...) { } // 必須是最后一個catch 子句
?
9. 為什么 catch 子句的異常聲明通常被聲明為引用?
1)可以避免由異常對象到 catch 子句中的對象的拷貝,特別是對象比較大時。
2)能確保catch子句對異常對象的修改能再次拋出。
3)確保能正確地調用與異常類型相關聯的虛擬函數,避免對象切割。
具體參見4,7,17。
?
10. 異常對象的生命周期?
產生:throw className()時產生。
銷毀:該異常的最后一個catch 子句退出時銷毀
注意:因為異常可能在catch子句中被重新拋出,所以在到達最后一個處理該異常的catch 子句之前,異常對象是不能被銷毀的。
?
11. const char *到char * 非法的異常類型轉換。
我們注意到下面的代碼在VC中可以正常運行(gcc不能)。
try { throw "exception";}
catch (char *) {cout << "exception catch!" <<endl;}
實際上throw的是一個const char *, catch的時候轉型成char *。這是C++對C的向下兼容。
同樣的問題存在于:
1. char *p =? “test”; // 也是一個const char * 到char *轉型。
2. void func(char* p) { printf("%s\n", p); }
??? func("abc"); // const char * 到char *
以上兩例在編譯時不警告,運行時不出錯,是存在隱患的。
?
12. 異常規范(exception specification)的概念?
異常規范在函數聲明是規定了函數可以拋出且只能拋出哪些異常。空的異常規范保證函數不會拋出任何異常。如果一個函數聲明沒有指定異常規范,則該函數可以拋出任何類型的異常。
例1:函數Pop若有異常,只能拋出popOnEmpty和string類型的異常對象
void pop( int &value ) throw(popOnEmpty, string);
例2:函數no_problem()保證不會拋出任何異常
extern void no_problem() throw();
例3:函數problem()可以拋出任何類型的異常
extern void problem();
?
13. 函數指針的異常規范?
我們也可以在函數指針的聲明處給出一個異常規范。例如:
void (*pf) (int) throw(string);
當帶有異常規范的函數指針被初始化或被賦值時,用作初始值或右值的指針異常規范必須與被初始化或賦值的指針異常規范一樣或更嚴格。例如:
void recoup( int, int ) throw(exceptionType);
void no_problem() throw();
void doit( int, int ) throw(string, exceptionType);
// ok: recoup() 與 pf1 的異常規范一樣嚴格
void (*pf1)( int, int ) throw(exceptionType) = &recoup;
// ok: no_problem() 比 pf2 更嚴格
void (*pf2)() throw(string) = &no_problem;
// 錯誤: doit()沒有 pf3 嚴格
void (*pf3)( int, int ) throw(string) = &doit;
注:在VC和gcc上測試失敗。
?
14. 派生類中虛函數的異常規范的聲明?
基類中虛擬函數的異常規范,可以與派生類改寫的成員函數的異常規范不同。但是派生類虛擬函數的異常規范必須與基類虛擬函數的異常規范一樣或者更嚴格。
class Base {
public:
virtual double f1( double ) throw ();
virtual int f2( int ) throw ( int );
virtual string f3( ) throw ( int, string );
// ...
};
class Derived : public Base {
public:
// error: 異常規范沒有 base::f1() 的嚴格
double f1( double ) throw ( string );
// ok: 與 base::f2() 相同的異常規范
int f2( int ) throw ( int );
// ok: 派生 f3() 更嚴格
string f3( ) throw ( int );
// ...
};
?
15. 被拋出的異常的類型和異常規范中指定的類型能進行類型轉換嗎?
int convert( int parm ) throw(string)
{
if ( somethingRather )
// 程序錯誤:
// convert() 不允許 const char* 型的異常
throw "help!";
}
throw 表達式拋出一個C 風格的字符串,由這個throw 表達式創建的異常對象的類型為const char*。通常,const char*型的表達式可以被轉換成string 類型。但是,異常規范不允許從被拋出的異常類型到異常規范指定的類型之問的轉換。
注意:
當異常規范指定一個類類型(類類型的指針)時,如果一個異常規范指定了一個類,則該函數可以拋出“從該類公有派生的類類型”的異常對象。類指針同理。
例如:
class popOnEmpty : public stackExcp { };
void stackManip() throw( stackExcp )? // 異常規范是stackExcp類型
{
??? throw stackExcp();??????????? // 與異常規范一樣
??? throw popOnEmpty ();????? // ok. 是stackExcp的派生類
}
?
16. 公有基類的catch子句可以捕捉到其派生類的異常對象。
int main( ) {
try {
// 拋出pushOnFull異常
}
catch ( Excp ) {
// 處理 popOnEmpty 和 pushOnFull 異常
throw;
}
catch ( pushOnFull ) {
// 處理 pushOnFull 異常
}
}
在上例中,進入catch ( Excp )子句,重新拋出的異常任然是pushOnFull類型的異常對象,而不會是其基類對象Excp。
?
17. 異常對象中怎么運用虛擬函數來完成多態?
1)異常申明是對象(不是引用或指針),類似于普通的函數調用,發生對象切割。
// 定義了虛擬函數的新類定義
class Excp {
public:
virtual void print() {
cerr << "An exception has occurred"
<< endl;
}
};
class stackExcp : public Excp { };
class pushOnFull : public stackExcp {
public:
virtual void print() {
cerr << "trying to push the value " << _value
<< " on a full stack\n";
}
// ...
};
int main( ) {
try {
// iStack::push() throws a pushOnFull exception
} catch ( Excp eObj ) {
eobj.print(); // 調用虛擬函數
// 喔! 調用基類實例
}
}
對象切割過程:eObj 以“異常對象的基類子對象Excp 的一個拷貝”作為初始值,eobj 是Excp 類型的對象,而不是pushOnFull 類型的對象。
輸出結果:
An exception has occurred
2)異常聲明是一個指針或引用
int main( ) {
try {
// iStack::push() 拋出一個 pushOnFull 異常
}
catch ( Excp &eObj ) {
eobj.print(); // 調用虛擬函數 pushOnFull::print()
}
}
輸出結果:
trying to push the value 879 on a full stack
?
18. function try block(函數try塊)
把整個函數體包含在一個try塊中
int main()
try {
// main() 的函數體
}
catch ( pushOnFull ) {
// ...
}
catch ( popOnEmpty ) {
// ...
}
?
19. 為什么類的構造函數需要函數try塊?
如下例,普通的try塊
inline Account::無法處理成員初始化表中的異常,若serviceCharge拋出異常,則這個異常無法被捕捉到。
Account( const char* name, double opening_bal )
: _balance( opening_bal - serviceCharge() )
{
try {
_name = new char[ strlen(name)+1 ];
strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
}
catch ( ...) {
// 特殊處理
// 不能捕獲來自成員初始化表的異常
}
}
改進后如下,使用函數try 塊是保證“在構造函數中捕獲所有在對象構造期間拋出的異常”的惟一解決方案。關鍵字try 應該被放在成員初始化表之前,try 塊的復合語句包圍了構造函數體。
inline Account::
Account( const char* name, double opening_bal )
try
: _balance( opening_bal - serviceCharge() )
{
_name = new char[ strlen(name)+1 ];
strcpy( _name, name );
_acct_nmbr = get_unique_acct_nmbr();
}
catch ( ... )
{
// 特殊處理
// 現在能夠捕獲來自 ServiceCharge() 的異常了
}
?
參考文獻:
C++ Primer第三版
?
總結
以上是生活随笔為你收集整理的C++异常处理机制详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++异常处理机制示例及讲解
- 下一篇: C/C++中的运算符优先级总结