友元類、巢狀類、異常、執行階段型別識別(RTTI)、型別轉換運算子dynamic_cast, static_cast, const_cast, reiterpret_cast
1.友元類
1)格式 p489
friend class ClassName;
友元類的所有方法都可以訪問原始類的私有成員和保護成員。
2)例子:Tv 類和 Remote (遙控器)類
將 Remote 類宣告為 Tv 類的友元類;這同時意味著 Remote 類中有關於 Tv 類的程式碼,因此編譯器必須先了解 Tv 類後,才能處理 Remote 類,因此先定義 Tv 類:
class Tv { public: friend class Remote; // Remote can access Tv private parts ... private: int channel; ... };class Remote { public: void set_chan(Tv & t, int c) {t.channel = c;} // Remote 類中可以直接訪問 Tv 類的私有成員 ... };
3)可以選擇讓特定的類成員成為另一個類的友元,而不必讓整個類成為友元 -- 使用前向宣告(forward declaration)p492
在上述例子中,唯一直接訪問 Tv 成員的 Remote 方法是 Remote::set_chan(),因此它是唯一需要作為友元的方法;可以將該函式設定為 Tv 類的友元,但需要注意各種宣告和定義的順序:
讓 Remote::set_chan() 成為 Tv 類的友元,就需要在 Tv 類中將其宣告為友元:
class Tv { friend void Remote::set_chan()(Tv & t, int c); ... };
要是編譯器能夠處理這條語句,編譯器必須知道 Remote 類的定義,因此 Remote 類的定義需要放在 Tv 類的前面;
但是 Remote 類中的 set_chan() 方法用到了 Tv 類中的成員 channel,因此 Tv 類也應在 Remote 類之前定義;
避開這種迴圈依賴的方法是使用前向宣告,在 Remote 定義的前面插入下面的語句:
class Tv; //froware declaration class Remote {...};class Tv {...};
注意,不能使用這樣的順序:
class Remote; //forward declaration class Tv {...}; class Remote {...};
因為編譯器在 Tv 類的宣告中看到 Remote 的 set_chan() 方法被宣告為 Tv 類的友元之前,應該先看到 Remote 類的宣告和 set_chan() 方法的宣告。
使用正確的順序後,還需要注意:
如果此時 Remote 類中有 內聯方法程式碼:
class Remote { ... public: void onoff(Tv & t) {t.onoff();} void set_chan(Tv & t, int c) {t.channel = c;} ... };
這將呼叫 Tv 的 onoff() 方法以及使用 Tv 的私有成員 channel,所以此時編譯器必須已經看到了 Tv 類的宣告,這樣才能知道 Tv 有哪些方法和成員,但是 Tv 類的宣告在 Remote 類的宣告的後面;
解決該問題的方法是,使 Remote 宣告中只包含方法宣告,並將實際的定義放在 Tv 類之後;這樣,排列順序如下:
class Tv; class Remote //僅包含方法的宣告 { ... public: void onoff(Tv & t); void set_channel(Tv & t, int c); ... }; class Tv {...};
//將 Remote 類中方法的定義放在此處,通過在方法定義中使用 inline 關鍵字,仍然可以使其成為內聯方法
注意,讓整個 Remote 類成員友元並不需要前向宣告,因為友元語句本身已經指出 Remote 是一個類:p493
friend class Remote;
4)兩個類互為友元類 p494
5)某個成員為兩個類共同的友元 p495
2.巢狀類 p495
C++ 中可以將類宣告放在另一個類中;在另一個類中宣告的類被稱為巢狀類。
包含類 的成員函式可以建立和使用被巢狀類的物件;僅當宣告位於公有部分,才能在包含類的外面使用巢狀類,而且必須使用作用域解析運算子。
對類進行巢狀不建立類成員,僅僅是定義了一種型別,該型別(如果不是在共有部分宣告)僅在包含巢狀類宣告的類總有效。
3.異常
1)若出現了除以 0 等異常情況,編譯器的處理方法:p500
- 呼叫 abort() 函式。abort() 函式原型位於標頭檔案 csdlib(或 stdlib.h)中,其典型實現是向標準錯誤流(即 cerr 使用的錯誤流)傳送訊息 abnormal program termination,然後終止程式;呼叫 abort() 函式時將直接終止程式,而不是先返回到 main()。
- 返回錯誤碼。
- 異常機制
2)異常機制
異常提供了將控制權從程式的一個部分傳遞到另一個部分的途徑。對異常的處理有 3 個組成部分:
- 引發異常
- 使用處理程式捕獲異常
- 使用 try 塊
其中:
try 塊標識後可跟一個多個 catch 塊。 p502
throw 關鍵字表示引發異常,緊跟隨其後的值(如字串或物件)指出了異常的特徵,如 p502
int main() { ... try { z = hmean(x, y); } catch (const char * s) // 將 throw 後的字串的地址賦給指標 s { cout << s <<endl; ... } ... double hmean(double a, double b) { if (a == -b) throw "bad hmean() arguments: a = -b not allowed"; return 2.0 * a * b / (a + b); } ... }
異常型別可以是字串,或者是其他 C++ 型別;通常為類型別。 p503
注意,throw 不是將控制權返回給呼叫函式,而是導致程式沿函式呼叫序列後退,直到找到包含 try 塊的函式。在上述列子中,throw 將程式控制權返回給 main();程式將在 main() 中尋找與引發的異常型別匹配的異常處理程式(位於 try 塊後面的 catch 塊)。p503
如果函式引發了異常,但沒有 try 塊或沒有匹配的處理程式時,程式最終將呼叫 abort() 函式。
3)棧解退 p506
在處理函式呼叫時,程式將呼叫函式的指令的地址(返回地址)放在棧中。當被呼叫的函式執行完畢後,程式將使用該地址來確定從哪裡開始繼續執行。
如果函式由於出現了異常而終止,程式將釋放棧中的記憶體,但不會在釋放棧的第一個返回地址後停止,而是繼續釋放棧,知道找到一個位於 try 塊中的返回地址;隨後,控制權將轉到塊尾的異常處理程式。這個過程被成為棧解退。
棧解退機制的一個非常重要的特性是,和函式返回一樣,對於棧中的自動類物件,類的解構函式將被呼叫。
函式返回僅僅處理該函式放在棧中的物件,throw 語句處理 try 塊和 throw 之間整個函式呼叫序列放在棧中的物件。
4)throw 語句返回控制權 p510
throw 語句將控制權向上返回到第一個這樣的函式:包含能夠捕獲相應異常的 try-catch 組合。
此外
try { hm = hmean(a, b); ... } catch (bad_hmean & bg) { bg.mesg(); cout << "Cayght in means() \n"; throw; //rethrows the exception in hmean(a, b) }
catch 中的 throw; 語句用來將捕獲到的 hmean(a, b) 中的異常再次向上傳遞。p509
5)
引發異常時編譯器總是建立一個臨時拷貝。p510
class problem {...}; ... void super() throw (problem) //異常規範 p506 { ... if (oh_no) { problem oops; throw oops; //引發異常,編譯器建立一個臨時拷貝 ... } ... try { super(); } catch (problem & p) // p 指向 oops 的副本而不是 oop 本身 { ... }
catch 後面使用 problem & p 而不是 problem p 的原因是,基類引用可以指向派生類物件。因此使用基類引用能夠捕獲任何異常物件。
6)exception 類 p511
exception 標頭檔案定義了 exception 類,可以把它用作其他異常類的基類。exception 類中有一個名為 what() 的虛擬函式,它返回一個字串,該字串的特徵隨實現而異;因為它是一個虛擬函式,因此可以在從 exception 類派生而來的類中重新定義它:
#include <exception> ... class bad_hmean : public exception { public: const char * what() {return "bad arguments to hmean()";} ... }; class bad_gmean : public exception { public: const char * what() {return "bad arguments to gmean()";}\ ... };
基於 exception 的異常型別:
a. stdexcept 異常類 p512
標頭檔案 stdexcept 定義了一些異常類;首先,該檔案定義了 logic_error 和 runtime_error 類,它們都是以公有方式從 exception 派生而來的
class logic_error : public exception { public: explicit logic_error(const string & what_arg); ... }; class domain_error : public logic_error { public: expolicit domain_error(const string & what_arg); ... };
注意,這些類的建構函式都接受一個 string 物件作為引數,該引數提供了方法 what() 以 C 風格字串方式返回的字元資料。p512
異常類 logic_error 描述了典型的邏輯錯誤。logic_error 派生的每個類的名稱指出了它們用於報告的錯誤型別
- domain_error
- invalid_argument
- length_error
- out_of_bounds
每個類都有一個類似於 logic_error 的建構函式,使得我們能夠提供一個供方法 what() 返回的字串。
runtime_error 類派生的每個類描述了執行期間發生的錯誤
- range_error
- overflow_error
- underflow_error
b. bad_alloc 異常和 new p513
對於使用 new 導致的記憶體分配問題,C++ 的最新處理方式是讓 new 引發 bad_alloc 異常。
c. 空指標 和 new p514
C++ 標準也提供了一種在 new 導致的記憶體分配問題 失敗時返回空指標的 new。
7)異常、類和繼承 p514
8)意外異常(帶異常規範的函式產生的異常與規範列表中的異常不匹配)和未捕獲異常 p517
9)動態分配和異常 p519
void test2(int n) { double * ar = new double(n); ... if(oh_no) throw exception(); ... delete [] ar; return; }
上述程式碼中,進行棧解退時,將刪除棧中的變數 ar;但函式過早地終止意味著函式末尾的 delete[] 語句不會被執行,因此 ar 指向的記憶體塊並未被釋放,產生了記憶體洩漏。可以進行如下改進:
void test3(int n) { double * ar = new double(n); ... try { if (oh_no) throw exception(); } catch (exception & ex) { delete [] ar; throw; //重新 throw 原異常 } ... delete [] ar; return;
另一種方法是使用 智慧指標模板(ch16)。
4.執行階段型別識別(Runtime Type Identification)p520
C++ 中支援 RTTI 的 3 個元素:
- 如果可能的話,dynamic_cast 運算子將使用一個指向基類的指標來生成一個指向派生類的指標;否則,該運算子返回 0——空指標。
- typeid 運算府返回一個指出物件的型別的型別的值。
- type_info 結構儲存了有關特定型別的資訊。
只能將 RTTI 用於包含虛擬函式的類層次結構,因為只有對於這種類層次結構,才應該將派生物件的指標賦給基類指標。
1)dynamic_cast 運算子
dynamic_cast 運算子能夠回答“是否可以安全地將物件的地址賦給特定型別的指標”。
語法:
Superb * bm = dynamic_cast<Superb *>(pg);
其中 pg 指向一個物件。
上述語句表明:指標 pg 的型別是否可以安全地被轉換為 Superb *? 如果可以,運算子將返回物件的地址,否則返回一個空指標。
例如,對於該語句:
dynamic_cast<Type *>(pt);
通常,如果指向的物件(*pt)的型別為 Type 或者是從 Type 直接或間接派生而來的型別,則上面的表示式將指標pt轉換為 Tpye 型別的指標;否則,結果為0,即空指標。
也可以將 dynamic_cast 用於引用,例如:
Superb & rs = dynamic_cast<Superb &>(rg);
但沒有與空指標對應的引用值,因此無法使用特殊的引用值來指示失敗。當請求不正確時,dynamic_cast 將引發型別為 bad_cast 的異常;該異常是從 exception 類派生而來的,它是在標頭檔案 typeinfo 中被定義的。 p523
2)typeid 運算子和 type_info 類 p524
typeid 運算子能夠確定兩個物件是否為同種型別,它可以接受兩種引數:
- 類名
- 結果為物件的表示式
typeid 運算子返回一個對 type_info 物件的引用,而 type_info 類過載了 == 和 != 運算子,因此可以使用這些運算子來對型別進行比較。
例如,如果 pg 指向的是一個 Magnificent 物件,則下述表示式的結果為 bool 值 true,否則為 false:
typeid(Magnificent) == typeid(*pg);
如果 pg 指向的是一個空指標,程式將引發 bad_typeid 異常。
type_info 類包含一個 name() 成員,該函式返回一個隨實現而異的字串:通常是類的名稱。如,下面的語句顯式指標 pg 指向的物件所屬的類定義的字串
cout << "Now processing type " << typeid(*pg).name() << ".\n";
5.型別轉換運算子 p526
- dynamic_cast
- const_cast
- static_cast
- reinterpret_cast