1. 程式人生 > 其它 >友元類、巢狀類、異常、執行階段型別識別(RTTI)、型別轉換運算子dynamic_cast, static_cast, const_cast, reiterpret_cast

友元類、巢狀類、異常、執行階段型別識別(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