1. 程式人生 > >第15章 友元、異常和其他

第15章 友元、異常和其他

本章內容包括:

  • 友元類
  • 友元類方法
  • 巢狀類
  • 引發異常,try塊和catch塊
  • 異常類
  • 執行階段型別識別(RTTI)
  • dynamic_cast和typeid
  • static_cast,const_cast和reiterpret_cast

RTTI是一種確定物件型別的機制.新的型別轉換運算子提高了型別轉換的安全性.

15.1 友元

  • 也可以將類作為友元,在這種情況下,友元類的所有方法都可以訪問原始類的私有成員和保護成員.
  • 儘管友元被授予從外部訪問類的私有部分的許可權,但它們並不與面向物件的程式設計思想相悖;相反,它們提高了公有介面的靈活性.

15.1.1 友元類

  • 電視機和遙控器的例子
  • 程式清單15.1 tv.h 
    • 友元宣告可以位於公有,私有或保護部分,其所在的位置無關緊要.
  • 程式清單15.2 tv.cpp
  • 程式清單15.3 use_tv.cpp

15.1.2 友元成員函式

  • 程式清單15.4 tvfm.h
  • 必須小心排列各種宣告和定義的順序

15.1.3 其他友元關係 
15.1.4 共同的友元

  • 需要使用友元的另一種情況是,函式需要訪問兩個類的私有資料.

15.2 巢狀類

  • 對類進行巢狀與包含並不同.包含意味著將類作為另一個類的成員,而對類進行巢狀不建立類成員,而是定義了一種型別,該型別僅在包含巢狀類宣告的類中有效.
  • 對類進行巢狀通常是為了幫助實現另一個類,並避免名稱衝突.

15.2.1 巢狀類和訪問許可權

  1. 作用域 
    • 如果巢狀類是在另一個類的私有部分宣告的,則只有後者知道它.
    • 如果巢狀類是在另一個類的保護部分宣告的,則它對於後者來說是可見的,但是對於外部世界則是不可見的.然而,在這種情況中,派生類將指導巢狀類,並可以直接建立這種型別的物件.
    • 如果巢狀類是在另一個類的公有部分宣告的,則允許後者,後者的派生類以及外部世界使用它,因為它是公有的.然而,由於巢狀類的作用域為包含它的類,因此在外部世界使用它時,必須使用類限定符.
  2. 訪問控制 
    • 對巢狀類訪問權的控制規則與對常規類相同.

15.2.2 模板中的巢狀

  • 程式清單15.5 queuetp.h
  • 程式清單15.6 nested.cpp

15.3 異常

  • 異常是相對較新的C++功能,有些老式編譯器可能沒有實現.另外,有些編譯器預設關閉這種特性,您可能需要使用編譯器選項來啟用它.

15.3.1 呼叫abort

  • 程式清單15.7 error1.cpp

15.3.2 返回錯誤碼

  • 一種比異常終止更靈活的方法是,使用函式的返回值來指出問題.
  • 程式清單15.8 error2.cpp
  • 傳統的C語言數學庫使用的就是這種方法,它使用的全域性變數名為errno.當然,必須確保其他函式沒有將該全域性變數用於其他目的.

15.3.3 異常機制

  • 異常提供了將控制權從程式的一個部分傳遞到另一部分的途徑.
  • 對異常的處理有3個組成部分: 
    • 引發異常
    • 使用處理程式捕獲異常
    • 使用try塊
  • 程式使用異常處理程式exception handler來捕獲異常,異常處理程式位於要處理問題的程式中.catch關鍵字表示捕獲異常.
  • 程式清單15.9 error3.cpp
  • 執行throw語句類似於執行返回語句,因為它也將終止函式的執行;但throw不是將控制權返回給呼叫程式,而是導致程式沿函式呼叫序列後退,直到找到包含try塊的函式.執行完try塊中的語句後,如果沒有引發任何異常,則程式跳過try塊後面的catch塊,直接執行處理程式後面的第一條語句.
  • 如果函式引發了異常,而沒有try塊或沒有匹配的處理程式時,將會發生什麼情況.在預設情況下,程式最終將呼叫abort()函式,但可以修改這種行為.

15.3.4 將物件用作異常型別

  • 程式清單15.10 exc_mean.h
  • 程式清單15.11 error4.cpp

15.3.5 異常規範和C++11

  • 異常規範,這是C++98新增的一項功能,但C++11卻將其摒棄了.這意味著C++11仍然處於標準之中,但以後可能會從標準中剔除,因此不建議您使用它.
  • 然而,C++11確實支援一種特殊的異常規範:您可使用新增的關鍵字noexcept指出函式不會引發異常;還有運算子noexcept(),它判斷其運算元是否會引發異常.

15.3.6 棧解退

  • 假設try塊沒有直接呼叫引發異常的函式,而是呼叫了對引發異常的函式進行呼叫的函式,則程式流程將從引發異常的函式調到包含try塊和處理程式的函式.C++通常通過將資訊放在棧中來處理函式呼叫.
  • 引發機制的一個非常重要的特性是,和函式返回一樣,對於棧中的自動類物件,類的解構函式將被呼叫.
  • 程式清單15.12 error5.cpp
  • 異常極其重要的一點:程式進行棧解退以回到能夠捕獲異常的地方時,將釋放棧中的自動儲存型變數.如果變數是類物件,將為該物件呼叫解構函式.

15.3.7 其他異常特性

  • 既然throw語句將生成副本,為何程式碼中使用引用呢?畢竟,將引用作為返回值的通常原因是避免建立副本以提高效率.答案是,引用還有另一個重要特徵:基類引用可以執行派生類物件.假設有一組通過繼承關聯起來的異常型別,則在異常規範中只需列出一個基類引用,它將與任何派生類物件匹配.
  • 提示:如果有一個異常類繼承層次結構,應這樣排列catch塊:將捕獲位於層次結構最下面的異常類的catch語句放在最前面,將捕獲基類異常的catch語句放在最後面.
  • 在catch語句中使用基類物件時,將捕獲所有的派生類物件,但派生特性將被剝去,因此將使用虛方法的基類版本.

15.3.8 exception類

  • stdexcept異常類 
    • 異常類系列logic_error描述了典型的邏輯錯誤.
    • 異常invalid_argument指出給函式傳遞了一個意料外的值.
    • 異常length_error用於指出沒有足夠的空間來執行所需的操作.
    • 異常out_of_bounds通常用於指示索引錯誤.
    • runtime_error異常系列描述了可能在執行期間發生但難以預計和防範的錯誤.
  • bad_alloc異常和new

    • 對於使用new導致的記憶體分配問題,C++的最新處理方式是讓new引發bad_alloc異常.
    • 程式清單15.13 newexcp.cpp
  • 空指標和new

    • C++標準提供了一種在失敗時返回空指標的new,其用法如下: 
      int * pi = new (std::nothrow) int ;
      int * pa = new (std::nowthrow) int[500];

15.3.9 異常,類和繼承

  • 異常,類和繼承以三種方式相互關聯.首先,可以像標準C++庫所做的那樣,從一個異常類派生出另一個;其次,可以在類定義中巢狀異常類宣告來組合異常;第三,這種巢狀宣告本身可被繼承,還可用作基類.
  • 程式清單15.14 sales.h
  • 程式清單15.15 sales.cpp
  • 程式清單15.16 use_sales.pp

15.3.10 異常何時會迷失方向

  • 異常被引發後,在兩種情況下,會導致問題. 
    • 首先,如果它是在帶異常規範的函式中引發的,則必須與規範列表中的某種異常匹配(在繼承層次結構總,類型別與這個類及其派生類的物件匹配),否則稱為意外異常.
    • 如果異常不是在函式中引發的(或者函式沒有異常規範),則必須捕獲它.如果沒被捕獲(在沒有try塊或沒有匹配的catch塊時,將出現這種情況,則異常被稱為未捕獲異常.
  • 然而,可以修改程式對意外異常和未捕獲異常的反應.

    • 未捕獲異常不會導致程式立刻異常終止.相反,程式將首先呼叫函式terminate().在預設情況下terminate()呼叫abort()函式.可以指定terminate()應呼叫的函式(而不是abort())來修改terminate()的這種行為.為此,可呼叫set_terminate()函式.set_terminate()和terminate()都是在標頭檔案exception中宣告的.
    typedef void (*terminate_handler)();
    terminate_handler set_terminate(terminate_handler f) throw(); //C++98
    terminate_handler set_terminate(terminate_handler f) noexcept; //C++11
    void terminate(); //C++98
    void terminate() noexcept; //C++11
    • 其中的typedef使terminate_handler成為這樣一種型別的名稱:指向沒有引數和返回值的函式的指標.set_terminate()函式將不帶任何引數且返回型別為void的函式的名稱(地址)作為引數,並返回該函式的地址.如果呼叫了set_terminate()函式多次,則terminate()將呼叫最後一次set_terminate()呼叫設定的函式.
    • 原則上 ,異常規範應包含函式呼叫的其他函式引發的異常.
    • unexpected()函式和set_unexpected()函式處理異常的情景.與提供給set_terminate()的函式的行為相比,提供給set_unexpected()的函式的行為受到更嚴格的限制.具體的說,unexpected_handler函式可以: 
      • 通過呼叫terminate()(預設行為),abort()或exit()來終止程式.
      • 引發異常.引發異常(第二種選擇)的結果取決於unexpected_handler函式所引發的異常以及引發意外異常的函式的異常規範.
      • 如果新引發的異常與原來的異常規範匹配,則程式將從哪裡開始進行正常處理,即尋找與新引發的異常匹配的catch塊.基本上,這種方法將用預期的異常取代意外異常;
      • 如果新引發的異常與原來的異常規範不匹配,且一行規範彙總沒有包括std::bad_exception型別,則程式將呼叫terminate().bad_exception是從exception派生而來的,其生命位於標頭檔案exception中;
      • 如果新引發的異常與原來的異常規範不匹配,且原來的異常規範中包含了std::bad_exception型別,則不匹配額異常江北std::bad_exception異常所取代.
      • 總之,如果要補貨所有的異常(不管是預期的異常還是意外異常),則可以這樣做: 
        首先確保異常標頭檔案的宣告可用
#include <exception>
using namespace std;
  • 然後,設計一個替代函式將意外異常轉換為bad_exception異常
void myUnexpected()
{
    throw std::bad_exception();//or just throw;
}
  • 僅使用throw,而不指定異常將導致重新引發原來的異常.然而,如果異常規範中包含了這種型別,則該異常將被bad_exception物件所取代.
  • 接下來在程式的開始位置將意外異常操作制定為呼叫該函式:set_unexpected(myUnexpected);
  • 最後,將bad_exception型別包括在異常規範中,並新增如下catch塊序列:
double Argh(double,double) throw(out_of_bounds,bad_exception);
...
try{
    x = Argh(a, b);
}
catch(out_of_bounds & ex)
{
    ...
}
catch(bad_exception & ex)
{
    ...
}

15.3.11 有關異常的注意事項

  • 從前面關於如何使用一場的討論可知,應在設計程式時就加入異常處理功能,而不是以後再新增.這樣做有些缺點.例如:是歐諾個異常會增加程式程式碼,降低程式的執行速度.異常規範不適用於模板,因為模板函式引發的異常可能隨特定的具體化而異.異常和動態記憶體分配並非總能協同工作.
void test2(int n)
{
    double * ar = new double(n);
    ...
    if (oh_no)
        throw exception();
    ...
    delete [] ar;
    return;
}
  • 這裡有個問題.解退棧時,將刪除棧中的變數ar.單函式過早的終止意味著函式末尾的delete[]語句被忽略.指標消失了,但它指向的記憶體塊未被釋放,並且不可訪問.總之,這些記憶體被洩漏了.這些洩漏是可以避免的.例如:可以在引發異常的函式中補貨該異常,在catch塊中 包含一些清理程式碼,然後重新引發異常.
  • 雖然異常處理對於某些專案極為重要,但它也會增加程式設計的工作量,增大程式,降低程式的速度.另一方面,不進行錯誤檢查的程式碼可能非常高.

15.4 RTTI

  • RTTI是執行階段型別識別(Runtime Type Identification)的簡稱.RTTI旨在為程式在執行階段確定物件的型別提供一種標準方式.

15.4.1 RTTI的用途 
15.4.2 RTTI的工作原理

  • C++有3個支援RTTI的元素 
    • 如果可能的話,dynamic_cast運算子將使用一個紙箱積累的指標來生成一個指向派生類的指標;否則,該運算子返回0–空指標.
    • typeid運算子返回一個指出物件的型別的值.
    • type_info結構儲存了有關特定型別的資訊.
  • 只能講RTTI用於包含虛擬函式的類層次結構,原因在於只有對於這種類層次結構,才應該講派生物件的地址賦給基類指標.
  • 警告:RTTI只適用於包含虛擬函式的類.
  • RTTI的3個元素 
    • dynamic_cast運算子 
      • dynamic_cast是最常用的RTTI元件,它不能回答”指標指向的是哪類物件”這樣的問題,但能夠回答”是否可以安全地將物件的地址賦給特定型別的指標”這樣的問題.
      • 注意,與問題”指標指向的是哪種型別的物件”相比,問題”型別轉換是否安全”更通用,也更有用.通常想知道型別的原因在於:知道型別後,就可以知道呼叫特定的方法是否安全.要呼叫方法,型別並不一定要完全匹配,而可以是定義了方法的虛擬版本的基類型別.
      • 注意:通常,如果指向的物件(*pt)的型別為Type或者是從Type直接或間接派生而來的型別,則下面的表示式將指標pt轉換為Type型別的指標:C++ dynamic_cast<Type *>(pt)否則,結果為0,即空指標.
      • 程式清單15.17 rtti1.cpp 
        • 注意:即使編譯器支援RTTI,在預設情況下,它也可能關閉該特性.如果該特性被關閉,程式可能仍能夠通過編譯,但將出現執行階段錯誤.在這種情況下,您應檢視文件或選單選項.
        • 15.17中程式說明了重要的一點,即應儘可能使用虛擬函式,而只在必要時使用RTTI.
    • typeid運算子和type_info類 
      • typeid運算子使得能夠確定兩個物件是否為同種型別.它與sizeof有些相像,可以接受兩種引數 
        • 類名;
        • 結果為物件的表示式.
      • typeid(Magnificent) == typeid(*pg),如果pg是一個空指標,程式將引發bad_typeid異常.該異常型別是從exception類派生而來的,是在標頭檔案typeinfo中宣告的.
      • 程式清單 15.18 rtti2.cpp
    • 誤用RTTI的例子 
      • C++界有很多人對RTTI口誅筆伐,他們認為RTTI是多餘的,是導致程式效率低下和糟糕程式設計方式的罪魁禍首.
      • 提示:如果發現在擴充套件的if else語句系列中使用了typeid,則應考慮是否應該使用虛擬函式和dynamic_cast.

15.5 型別轉換運算子

  • 在允許C語言中的3種類型轉換情況下,Stroustrop採取的措施是,更嚴格地限制允許的型別轉換,並新增4個型別轉換運算子,使轉換過程更規範: 
    • dynamic_cast
    • const_cast
    • static_cast
    • reinterpret_cast
  • 可以根據目的選擇一個適合的運算子,而不是使用通用的型別轉換.這指出了進行型別轉換的原因,並讓編譯器能夠檢查程式的行為是否與設計者想法吻合.
  • dynamic_cast < type-name > (expression) 
    • 該運算子的用途是,使得能夠在類層次結構中進行向上轉換(由於is-a關係,這樣的型別轉換是安全的),而不允許其他轉換.
  • const_cast運算子用於執行只有一種用途的型別轉換,即改變值為const或volatile,其語法與dynamic_cast運算子相同:const_cast (expression),由於程式設計時可能無意間同時改變型別和常量特徵,因此使用const_cast運算子更安全.const_cast不是萬能的.它可以修改指向一個值的指標,但修改const值的結果是不確定的. 
    • 程式清單15.19 constcast.cpp
  • static_cast運算子的語法與其他型別轉換運算子相同:static_cast < type-name> (expression),僅當type_name可被隱式轉換為expression所屬的型別或exptession可被隱式轉換為type_name所屬的型別時,上述轉換才是合法的,否則將出錯.
  • reinterpret_cast運算子用於天生危險的型別轉換.它不允許刪除const,但會執行其他令人生厭的操作.reinterpret_cast < type-name > (expression),reinterpret_cast運算子並不支援所有的型別轉換.另一個 限制是,不能將函式指標轉換為資料指標,反之亦然.
  • 在C++中,普通型別轉換也受到限制.基本上,可以執行其他型別轉換科執行的操作,加上一些組合(如上面4種),但不能執行其他轉換.這些限制是合理的,如果您覺得這種限制難以忍受,可以使用C語言.

15.6 總結

  • 友元使得能夠為類開發更靈活的介面
  • 巢狀類是在其他類中宣告的類.
  • C++異常機制為處理拙劣的程式設計事件,如不適當的值,I/O失敗等,提供了一種靈活的方式.
  • RTTI(執行階段型別資訊)特性讓程式能夠檢測物件的型別.

15.7 複習題 
15.8 程式設計練習

本章原始碼下載地址