C++中理解“傳遞參數”和異常之間的差異

從語法上看,在函數裏聲明參數與在catch子句中聲明參數幾乎沒有什麼差別:
  class Widget { ... }; //一個類,具體是什麼類

    // 在這裏並不重要

    void f1(Widget w); // 一些函數,其參數分別爲

    void f2(Widget& w); // Widget, Widget&,或

    void f3(const Widget& w); // Widget* 類型

    void f4(Widget *pw);

    void f5(const Widget *pw);

    catch (Widget w) ... //一些catch 子句,用來

    catch (Widget& w) ... //捕獲異常,異常的類型爲

    catch (const Widget& w) ... // Widget, Widget&, 或

    catch (Widget *pw) ... // Widget*

    catch (const Widget *pw) ...
複製代碼     你因此可能會認爲用throw拋出一個異常到catch子句中與通過函數調用傳遞一個參數兩者基本相同。這裏面確有一些相同點,但是他們也存在着巨大的差異。
      讓我們先從相同點談起。你傳遞函數參數與異常的途徑可以是傳值、傳遞引用或傳遞指針,這是相同的。但是當你傳遞參數和異常時,系統所要完成的操作過程則是完全不同的。產生這個差異的原因是:你調用函數時,程序的控制權最終還會返回到函數的調用處,但是當你拋出一個異常時,控制權永遠不會回到拋出異常的地方。
      有這樣一個函數,參數類型是Widget,並拋出一個Widget類型的異常:
  // 一個函數,從流中讀值到Widget中

    istream operator>>(istream& s, Widget& w);

    void passAndThrowWidget()

    {

     Widget localWidget;

     cin >> localWidget; //傳遞localWidget到 operator>>

     throw localWidget; // 拋出localWidget異常

    }
複製代碼     當傳遞localWidget到函數operator>>裏,不用進行拷貝操作,而是把operator>>內的引用類型變量w指向localWidget,任何對w的操作實際上都施加到localWidget上。這與拋出localWidget異常有很大不同。不論通過傳值捕獲異常還是通過引用捕獲(不能通過指針捕獲這個異常,因爲類型不匹配)都將進行lcalWidget的拷貝操作,也就說傳遞到catch子句中的是localWidget的拷貝。必須這麼做,因爲當localWidget離開了生存空間後,其析構函數將被調用。如果把localWidget本身(而不是它的拷貝)傳遞給catch子句,這個子句接收到的只是一個被析構了的Widget,一個Widget的“屍體”。這是無法使用的。因此C++規範要求被做爲異常拋出的對象必須被複制。
      即使被拋出的對象不會被釋放,也會進行拷貝操作。例如如果passAndThrowWidget函數聲明localWidget爲靜態變量(static),
  void passAndThrowWidget()

    {

     static Widget localWidget; // 現在是靜態變量(static);

     //一直存在至程序結束

     cin >> localWidget; // 象以前那樣運行

     throw localWidget; // 仍將對localWidget

    } //進行拷貝操作
複製代碼     當拋出異常時仍將複製出localWidget的一個拷貝。這表示即使通過引用來捕獲異常,也不能在catch塊中修改localWidget;僅僅能修改localWidget的拷貝。對異常對象進行強制複製拷貝,這個限制有助於我們理解參數傳遞與拋出異常的第二個差異:拋出異常運行速度比參數傳遞要慢。
      當異常對象被拷貝時,拷貝操作是由對象的拷貝構造函數完成的。該拷貝構造函數是對象的靜態類型(static type)所對應類的拷貝構造函數,而不是對象的動態類型(dynamic type)對應類的拷貝構造函數。比如以下這經過少許修改的passAndThrowWidget:

 

    class Widget { ... };

 

    class SpecialWidget: public Widget { ... };

 

    void passAndThrowWidget()

 

    {

 

    SpecialWidget localSpecialWidget;

 

    ...

 

    Widget& rw = localSpecialWidget; // rw 引用SpecialWidget

 

    throw rw; //它拋出一個類型爲Widget

 

    // 的異常

 

    }
複製代碼

    這裏拋出的異常對象是Widget,即使rw引用的是一個SpecialWidget。因爲rw的靜態類型(static type)是Widget,而不是SpecialWidget。你的編譯器根本沒有主要到rw引用的是一個SpecialWidget。編譯器所注意的是rw的靜態類型(static type)。這種行爲可能與你所期待的不一樣,但是這與在其他情況下C++中拷貝構造函數的行爲是一致的。

    異常是其它對象的拷貝,這個事實影響到你如何在catch塊中再拋出一個異常。比如下面這兩個catch塊,乍一看好像一樣:

 

    catch (Widget& w) // 捕獲Widget異常

 

    {

 

    ... // 處理異常

 

    throw; // 重新拋出異常,讓它

 

    } // 繼續傳遞

 

    catch (Widget& w) // 捕獲Widget異常

 

    {

 

    ... // 處理異常

 

    throw w; // 傳遞被捕獲異常的

 

    } // 拷貝
複製代碼

    這兩個catch塊的差別在於第一個catch塊中重新拋出的是當前捕獲的異常,而第二個catch塊中重新拋出的是當前捕獲異常的一個新的拷貝。如果忽略生成額外拷貝的系統開銷,這兩種方法還有差異麼?

    當然有。第一個塊中重新拋出的是當前異常(current exception),無論它是什麼類型。特別是如果這個異常開始就是做爲SpecialWidget類型拋出的,那麼第一個塊中傳遞出去的還是SpecialWidget異常,即使w的靜態類型(static type)是Widget。這是因爲重新拋出異常時沒有進行拷貝操作。第二個catch塊重新拋出的是新異常,類型總是Widget,因爲w的靜態類型(static type)是Widget。一般來說,你應該用throw來重新拋出當前的異常,因爲這樣不會改變被傳遞出去的異常類型,而且更有效率,因爲不用生成一個新拷貝。

    (順便說一句,異常生成的拷貝是一個臨時對象。正如條款19解釋的,臨時對象能讓編譯器優化它的生存期(optimize it out of existence),不過我想你的編譯器很難這麼做,因爲程序中很少發生異常,所以編譯器廠商不會在這方面花大量的精力。)

    讓我們測試一下下面這三種用來捕獲Widget異常的catch子句,異常是做爲passAndThrowWidgetp拋出的:

 

catch (Widget w) ... // 通過傳值捕獲異常

 

    catch (Widget& w) ... // 通過傳遞引用捕獲

 

    // 異常

 

    catch (const Widget& w) ... //通過傳遞指向const的引用

 

    //捕獲異常
複製代碼

    我們立刻注意到了傳遞參數與傳遞異常的另一個差異。一個被異常拋出的對象(剛纔解釋過,總是一個臨時對象)可以通過普通的引用捕獲;它不需要通過指向const對象的引用(reference-to-const)捕獲。在函數調用中不允許轉遞一個臨時對象到一個非const引用類型的參數裏,但是在異常中卻被允許。

    讓我們先不管這個差異,回到異常對象拷貝的測試上來。我們知道當用傳值的方式傳遞函數的參數,我們製造了被傳遞對象的一個拷貝(參見Effective C++ 條款22),並把這個拷貝存儲到函數的參數裏。同樣我們通過傳值的方式傳遞一個異常時,也是這麼做的。當我們這樣聲明一個catch子句時:

    catch (Widget w) ... // 通過傳值捕獲

    會建立兩個被拋出對象的拷貝,一個是所有異常都必須建立的臨時對象,第二個是把臨時對象拷貝進w中。同樣,當我們通過引用捕獲異常時catch (Widget& w) ... // 通過引用捕獲

    catch (const Widget& w) ... //也通過引用捕獲
複製代碼
      這仍舊會建立一個被拋出對象的拷貝:拷貝是一個臨時對象。相反當我們通過引用傳遞函數參數時,沒有進行對象拷貝。當拋出一個異常時,系統構造的(以後會析構掉)被拋出對象的拷貝數比以相同對象做爲參數傳遞給函數時構造的拷貝數要多一個。
      我們還沒有討論通過指針拋出異常的情況,不過通過指針拋出異常與通過指針傳遞參數是相同的。不論哪種方法都是一個指針的拷貝被傳遞。你不能認爲拋出的指針是一個指向局部對象的指針,因爲當異常離開局部變量的生存空間時,該局部變量已經被釋放。Catch子句將獲得一個指向已經不存在的對象的指針。這種行爲在設計時應該予以避免。
      對象從函數的調用處傳遞到函數參數裏與從異常拋出點傳遞到catch子句裏所採用的方法不同,這只是參數傳遞與異常傳遞的區別的一個方面,第二個差異是在函數調用者或拋出異常者與被調用者或異常捕獲者之間的類型匹配的過程不同。比如在標準數學庫(the standard math library)中sqrt函數:
    double sqrt(double); // from or
      我們能這樣計算一個整數的平方根,如下所示:
    int i;
    double sqrtOfi = sqrt(i);
      毫無疑問,C++允許進行從int到double的隱式類型轉換,所以在sqrt的調用中,i 被悄悄地轉變爲double類型,並且其返回值也是double。(有關隱式類型轉換的詳細討論參見條款5)一般來說,catch子句匹配異常類型時不會進行這樣的轉換。見下面的代碼:
    void f(int value)

    {

     try {

      if (someFunction()) { // 如果 someFunction()返回

      throw value; //真,拋出一個整形值

      ...

      }

     }

     catch (double d) { // 只處理double類型的異常

      ...

     }

     ...

    }
複製代碼
      在try塊中拋出的int異常不會被處理double異常的catch子句捕獲。該子句只能捕獲真真正正爲double類型的異常;不進行類型轉換。因此如果要想捕獲int異常,必須使用帶有int或int&參數的catch子句。
      不過在catch子句中進行異常匹配時可以進行兩種類型轉換。第一種是繼承類與基類間的轉換。一個用來捕獲基類的catch子句也可以處理派生類類型的異常。例如在標準C++庫(STL)定義的異常類層次中的診斷部分(diagnostics portion )(參見Effective C++ 條款49)。
      捕獲runtime_errors異常的Catch子句可以捕獲range_error類型和overflow_error類型的異常,可以接收根類exception異常的catch子句能捕獲其任意派生類異常。
      這種派生類與基類(inheritance_based)間的異常類型轉換可以作用於數值、引用以及指針上:
    catch (runtime_error) ... // can catch errors of type

    catch (runtime_error&) ... // runtime_error,

    catch (const runtime_error&) ... // range_error, or

    // overflow_error

    catch (runtime_error*) ... // can catch errors of type

    catch (const runtime_error*) ... // runtime_error*,

    // range_error*, or

    // overflow_error*
複製代碼
      第二種是允許從一個類型化指針(typed pointer)轉變成無類型指針(untyped pointer),所以帶有const void* 指針的catch子句能捕獲任何類型的指針類型異常:
      catch (const void*) ... //捕獲任何指針類型異常
      傳遞參數和傳遞異常間最後一點差別是catch子句匹配順序總是取決於它們在程序中出現的順序。因此一個派生類異常可能被處理其基類異常的catch子句捕獲,即使同時存在有能處理該派生類異常的catch子句,與相同的try塊相對應。例如:
try {

     ...

    }

    catch (logic_error& ex) { // 這個catch塊 將捕獲

     ... // 所有的logic_error

    } // 異常, 包括它的派生類

    catch (invalid_argument& ex) { // 這個塊永遠不會被執行

     ... //因爲所有的

    } // invalid_argument

    // 異常 都被上面的

    // catch子句捕獲。
複製代碼
      與上面這種行爲相反,當你調用一個虛擬函數時,被調用的函數位於與發出函數調用的對象的動態類型(dynamic type)最相近的類裏。你可以這樣說虛擬函數採用最優適合法,而異常處理採用的是最先適合法。如果一個處理派生類異常的catch子句位於處理基類異常的catch子句前面,編譯器會發出警告。(因爲這樣的代碼在C++裏通常是不合法的。)不過你最好做好預先防範:不要把處理基類異常的catch子句放在處理派生類異常的catch子句的前面。象上面那個例子,應該這樣去寫:
   try {

     ...

    }

    catch (invalid_argument& ex) { // 處理 invalid_argument

     ... //異常

    }

    catch (logic_error& ex) { // 處理所有其它的

     ... // logic_errors異常

    }
複製代碼
      綜上所述,把一個對象傳遞給函數或一個對象調用虛擬函數與把一個對象做爲異常拋出,這之間有三個主要區別。第一、異常對象在傳遞時總被進行拷貝;當通過傳值方式捕獲時,異常對象被拷貝了兩次。對象做爲參數傳遞給函數時不需要被拷貝。第二、對象做爲異常被拋出與做爲參數傳遞給函數相比,前者類型轉換比後者要少(前者只有兩種轉換形式)。最後一點,catch子句進行異常類型匹配的順序是它們在源代碼中出現的順序,第一個類型匹配成功的catch將被用來執行。當一個對象調用一個虛擬函數時,被選擇的函數位於與對象類型匹配最佳的類裏,即使該類不是在源代碼的最前頭。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章