從語法上看,在函數裡聲明參數與在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引用類型的參數裡,但是在異常中卻被允許。 // 因為臨時對象會過一會兒會釋放了,那麼函數中該非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子句匹配順序總是取決於它們在程序中出現的順序。因此一個派生類異常可能被處
理其基類異常的catch子句捕獲,這叫異常截獲,一般的編譯器會有警告。
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)最相近的
類裡。你可以這樣說虛擬函數匹配采用最優匹配法,而異常處理匹配采用的是最先匹配法。
在函數調用中不允許轉遞一個臨時對象到一個非const引用類型的參數裡,但是在異常中卻被允許。 // 因為臨時對象會過一會兒會釋放了,那麼函數中該非const引用就引用一個不存在的對象了。而當為const引用類型時,該臨時對象並不會過一會兒釋放,而是與函數中該參數生命周期一樣,這樣就可以引用該臨時對象了。
class A
{
public:
A()
{
m_Int = 10;
}
public:
int m_Int;
};
A GetInt()
{
A a;
return a;
}
void Test( A& pA )
{
cout << pA.m_Int << endl; // 還會輸出10 return;
}
int main()
{
A& pA = GetInt();
cout << pA.m_Int << endl; // 可以輸出10,是因為pA為引用,GetInt返回的臨時變量不會馬上釋放,生命周期與pA一致
Test( GetInt() ); // 這裡不是傳遞一個臨時變量到非const引用的參數,怎麼也行呢?
system("pause");
return 1;
}