12 瞭解“丟擲一個exception”與“傳遞一個引數”或“呼叫一個虛擬函式”之間的差異
函式引數和exception的傳遞方式有三種:by value,by reference,by pointer。然而視你所傳遞的是引數或exception,發生的事情完全不同。
原因:當你呼叫一個函式,控制權最終會回到呼叫端(除非函式失敗以至於無法返回),但當你丟擲一個exception,控制權不會再回到丟擲端。
有這樣的一個函式,引數型別是Widget,並丟擲一個Widget型別的異常:
//一個函式,從流中讀值到Widget中 istream operator >>(istream& is,Widget& w); void passAndThrowWidget() { Widget localWidget; cin >> localWidget; throw localWidget; }
當傳遞localWidget到函式operator>>裡,不用進行拷貝操,而是把operator>>內的引用型別變數w指向了localWidget,任何對w的操作實際都實施到localWidget本身上。這與丟擲localWidget異常有很大不同。不論通過傳遞值捕獲異常還是通過 引用捕獲(不能通過指標捕獲這個異常,因為型別不匹配)都將進行localWidget的拷貝操作,也就是說傳遞到catch子句中的localWidget是拷貝。必須這麼做,因為當localWidget離開生存空間後,其解構函式將被呼叫。如果localWidget本身傳遞給catch子句,這個子句接收到的時候被析構了的Widget,一個Widget的“屍體”。這是無法使用的。因此C++規定要求被作為異常丟擲的物件必須被複制。
即時被丟擲的物件不會被釋放,也會進行拷貝操作。例如passAndThrowWidget函式生命localWidget為靜態量(static):
void passAndThrowWidget()
{
static Widget localWidget;
cin >> localWidget;
throw localWidget;
}
當丟擲異常仍將複製出localWidget的一個拷貝。這表示即使通過引用來捕獲異常,也不能在catch中修改localWidget;僅僅能修改localWidget的拷貝。引數傳遞和丟擲異常第二個差異:丟擲異常執行速度比引數傳遞慢。
當異常物件被拷貝時,拷貝操作由拷貝建構函式完成。該拷貝建構函式是靜態型別的所對應的拷貝建構函式,而不是動態型別對應的拷貝建構函式。例如:
class Widget{};
class SpecialWidget:public Widget{}
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; //rw引用了localSpecialWidget,
thread rw; //它丟擲的異常是Widget
}
這裡丟擲的異常是Widget,即使rw引用了SpecialWidget,因為rw的靜態型別是Widget。
異常型別其物件的拷貝,這影響你如何在catch塊中再丟擲異常,比如下面的兩個catch塊:
catch(Widget& w)
{
...
throw; //捕獲異常重新丟擲
}
catch(Widget& w)
{
...
throw w; //捕獲異常傳遞的是拷貝
}
第一個catch重新丟擲的當前異常,無論它是什麼型別,如果w一開始是SpecialWidget,傳遞的還是SpecialWidget。
第二個catch重新丟擲異常的拷貝,型別總是Widget。
異常生成的拷貝是一個臨時物件。
如下有三種捕獲Widget異常的的catch子句,異常是作為passAndThrowWidget丟擲的:
catch(Widget w);
catch(Widget& w);
catch(const Widget w);
當第一個語句時候,會建立兩個被丟擲物件的拷貝,一個是所有異常的都必須建立的臨時物件,第二個是把臨時物件拷貝w中。(兩次拷貝)
第二、第三語句的時候,只會建立臨時物件。。
當丟擲一個異常時,系統構造的被丟擲物件的拷貝數比相同的物件作為引數傳遞給函式的構造的的拷貝數要多一次。
函式呼叫或丟擲異常或丟擲異常者與被呼叫者或異常捕獲者之間的型別匹配的過程不同。
比如標準數學庫的sqrt函式:
double sqrt(double);
我們能這樣計算一個整數的平方根:
int i ;
...
double sqrOfi = sqrt(i);
C++允許把int到double隱式轉換,所以i被悄悄變為double,並且其返回值也是double,一般來說catch子句匹配異常型別時不會進行這樣的轉換:
void f(int value)
{
try{
if(someFunction())
throw value; //丟擲的是int
}
catch(double d) //只能處理double的異常
{
...
}
...
}
上述只能捕獲double型別異常,而丟擲的是int型別的異常,因此要捕獲int型別的必須使用int型別或者int&型別引數的catch子句。
不過在catch子句進行異常匹配可以進行兩種轉換。第一種就是繼承類和基類之類的轉換,一個用來捕獲基類的catch子句也可以用來處理派生類型別的異常。
這種派生類和基類間的異常型別轉換可以用於數值、引用以及指標上面。
第二種允許從一個型別化指標轉換成無型別指標,所以帶有const void*的catch子句可以捕獲任何型別的指標型別異常:
catch(const void*); //捕獲任何指標型別的異常
傳遞引數和傳遞異常最後一個區別點:catch子句匹配順序總是取決於它們在程式中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有可能直接處理該派生類異常的catch子句,與相對應的try。例如:
try{
...
}
catch(logic_error& ex) //這個講捕獲所有的logic_error異常
{
...
}
catch(invalid_argument& ex) //這個塊永遠捕獲被執行,invalid_argument
{ //是logic_error子類
}
與上面這種行為相反的,當你呼叫一個虛擬函式,被呼叫的函式位於與發生函式呼叫的物件的動態型別最相近的類裡。即:虛擬函式採用最優法,而異常處理採用最先法。
所以上述程式碼應該先捕獲invalid_argument,再捕獲logic_error。
綜上所述,把一個物件傳遞給函式或一個物件呼叫虛擬函式與一個物件作為異常丟擲主要有三點區別:
- 異常物件在傳遞時總進行拷貝,當通過傳值方式捕獲異常時,異常物件被拷貝了兩次。物件作為引數傳遞給函式不一定需要被拷貝。
- 物件作為異常被丟擲與引數傳遞給函式相比,前者型別轉換比後者少(只有兩種轉換)。
- catch子句在進行異常型別匹配的順序是它們在原始碼中出現的順序,第一個型別匹配成功的catch被執行。當一個物件呼叫一個虛擬函式,被選擇的函式位於與物件型別匹配最佳的類裡,即使該類不在原始碼的最前頭。