第15章 友元、異常和其他
阿新 • • 發佈:2018-11-29
本章內容包括:
- 友元類
- 友元類方法
- 巢狀類
- 引發異常,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 巢狀類和訪問許可權
- 作用域
- 如果巢狀類是在另一個類的私有部分宣告的,則只有後者知道它.
- 如果巢狀類是在另一個類的保護部分宣告的,則它對於後者來說是可見的,但是對於外部世界則是不可見的.然而,在這種情況中,派生類將指導巢狀類,並可以直接建立這種型別的物件.
- 如果巢狀類是在另一個類的公有部分宣告的,則允許後者,後者的派生類以及外部世界使用它,因為它是公有的.然而,由於巢狀類的作用域為包含它的類,因此在外部世界使用它時,必須使用類限定符.
- 訪問控制
- 對巢狀類訪問權的控制規則與對常規類相同.
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];
- C++標準提供了一種在失敗時返回空指標的new,其用法如下:
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
- typeid運算子使得能夠確定兩個物件是否為同種型別.它與sizeof有些相像,可以接受兩種引數
- 誤用RTTI的例子
- C++界有很多人對RTTI口誅筆伐,他們認為RTTI是多餘的,是導致程式效率低下和糟糕程式設計方式的罪魁禍首.
- 提示:如果發現在擴充套件的if else語句系列中使用了typeid,則應考慮是否應該使用虛擬函式和dynamic_cast.
- 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 程式設計練習
本章原始碼下載地址