C++深入理解(8)------類繼承的詳述(讀書筆記)

1.基本概述:    

    首先說下總體的規則,相當於重點:派生類可以經過繼承後,擁有基類的所有數據成員和部分函數(不包括構造函數,賦值運算符和友元函數),但是擁有了,卻不一定能使用,這裏並不區分私有繼承還是公共繼承,都是隻能訪問基類中public和protected的成員。也就是說可不可以訪問,要看基類的中成員變量是否可以被訪問(public和protected),而不是看繼承之後屬性是否可以被訪問。

    經過公有繼承繼承後,原來的暴露屬性還是原來的屬性。私有繼承則是將繼承過來的所有public和protectd都變爲私有的屬性,也就是在第三代繼承後,第三代成員就不能在訪問基類中的共有方法了。

    在繼承時不管是什麼類型的繼承,都會將基類的所有數據成員和函數複製到子類的裏邊,是所有的,包括基類的私有成員和函數。隨意子類的成員變量就是子類的成員變量加上父類的成員變量。那麼在子類的構造函數的時候,就需要將所有的成員變量初始化。一般有兩種方法:

class RatedPlayer : public TableTennisPlayer

//方法1,將父類和子類的成員都列出來初始化
Ratedplayer::RatedPlayer(unsigned int r,const string & frr, const string & ln,bool ht):TableTennisplayer(fn , In , ht)
{
    firstname = fn;        //基類中的私有,不能顯示訪問,所以只能通過調用基類的構造函數
    lastname = In;         //基類中私有,不能這樣用
    hasTable = ht;         //基類中私有,不能這樣用,當然可以把初始化表放在下面
    //TableTennisplayer(fn,in,ht);
    rating = r;
}
//同樣是方法1,如果省略掉父類的幾個屬性,那麼相當於調用父類的默認構造函數。
Ratedplayer::RatedPlayer(unsigned int r,const string & frr, const string & ln,bool ht)
{
    rating = r;
}

//方法2,傳入父類的引用, 
RatedPlayer::RatadPlByer(unsigned int r, const tableTennisPlayer & tp):trableTennisPlayer(tp),rating(r){}

    在本書中,將繼承視爲先創建基類的對象,然後在創建子類的對象,所以在初始化的時候,也要先初始化基類中的屬性。因爲常常基類中屬性都是私有的,所以只能通過基類中共有的方法,如構造函數等。所以繼承的時候一定注意不僅僅要初始化自己的成員變量。在析構函數調用是,則是先調用子類的析構函數,然後在調用父類的析構函數。當然這個過程類似於構造函數的過程, 如果程序員不予以明確的聲明調用析構函數,那麼編譯器將自動調用默認析構函數,也就是唯一定義的那個。

2.派生類和基類之間的關係:

    a.派生類可以訪問基類中的共有方法
    b.基類指針可以指向派生類
    c.基類引用可以引用派生類。

    所以在作爲函數參數傳遞時,將派生類的指針作爲基類的參數傳遞過去,如下面的函數

void Show(const TableTennisPlayer & rt);
TableTennisPlayer playerl("Tara","Boopca", false);
RatedPlayer rplayerl (1140, "Mallory", "Duck", true);
Show (playerl );
Show (rplayerl);

    在使用指向派生類的基類指針的時候常常會遇到搞不清楚到底指針調用的是基類的方法還是派生類的方法。所以這裏說下,當基類中的方法並沒非是虛函數的時候,基類指針調用的都是基類自己的方法,但是遇到用虛函數的時候,就要引用派生類新的類型。

3.爲什麼需要虛析構函數:

    在派生類中的週期結束後,會調用析構函數,如果不是虛析構函數,在不使用指向基類的指針的時候並沒有什麼區別,都是先調用派生類的析構函數,在派生類的析構函數中會默認調用基類的析構函數。但是如果使用的是指向派生類的基類指針,在指針被銷燬的時候,理論上應該先調用派生類的指針,但是如果沒有虛析構函數,那麼就會造成直接調用基類的析構函數。造成構造函數和析構函數的不對應。

4.關於靜態關聯和動態關聯:

    一般的使用定義過程都是在靜態關聯,在定義一個變量的時候,編譯器就能夠將其翻譯爲目標文件的語言,但是在使用虛函數派生的時候,編譯器將不知道具體使用的是哪個派生類對象,所以使用的是動態關聯。

    靜態關聯的效率更高,因爲使用動態關聯實在運行的時候才知道具體的使用方式,所以必須使用一些方式來跟蹤基類指針的指向。所以在設計類的時候也不能爲了省事,而將所有方法都設計爲虛函數,可以根據要不要重寫這種方法來決定,可以增加效率。當然這有一定難度,因爲需求不定,不能知道具體的實現方式。

5.虛函數的原理--虛函數表

    虛函數是如何實現動態關聯的?虛函數底層的工作原理是什麼?虛函數使用的是虛函數表,具體就是沒一個類中都有一個隱藏的指針指向一個數組表,這個表中保存了所有虛函數的地址。在派生類中同樣存在着一個類似的虛函數表,但是此表與基類的表略有不同,如果派生類中沒有重寫虛函數,那麼此表對應位置的虛函數地址還是父類的地址,如果重寫了虛函數,那麼對應此函數的位置的地址值就是新的重寫的函數的地址。

    類似於基類A,有兩個虛函數virtual funcA_a(),virtual funcA_b(),那麼此表的值爲table={0x45,0x78}假設的值;

    派生類B,重寫funcA_b(),並新增funcB_c()。那麼此表的值爲table={0x45,0x96,0x99},瞎編的數值,只是表示相對地址改變。

    在每次函數的調用的時候,都會先訪問次對象中隱藏的虛函數表中此函數對應位置處的地址,然後確定其函數到底是哪個,然後在調用。在這個過程中就會更消耗資源。

6.繼承中需注意:

    a.構造函數不能是虛函數,如果是虛函數,在創建的時候就會調用調用繼承類的構造函數,就會出問題。
    b.析構函數一般設爲虛函數。

    c.重新定義將隱藏方法:重新定義是指名字與基類中的函數相同,但是參數卻不同,不管是虛函數還是普通函數,原來的函數都會被覆蓋。所以在定義函數的時候就需要嚴格依據基類中的名字做定義;同時如果一個相同名字的函數被重載了,那麼需要重載所有同名的函數。

7.派生類使用new

    在派生類的構造函數中使用new來初始化部分屬性,同時在基類中也有類似操作,就會出現問題。但是在使用基類的時候,我們往往無法看到基類的內容,所以這裏建議不管基類中是否應用new初始化屬性,都必須顯式的在派生類中聲明覆制構造函數,賦值運算符,而析構函數因爲會自動調用默認析構函數,所以並不需要特殊處理,只需銷燬自己的屬性就可以了。

    其實顯示聲明覆制構造函數,就是顯式的在開始調用一下基類的複製構造函數,而複製構造函數則是顯式的調用基類的賦值運算符。如下代碼:

baseDMA::operators(hs);

8.關於私有繼承

    私有繼承是將基類的成員繼承過來變爲派生類的私有成員,但是派生類只允許使用基類的共有和保護成員,私有成員只能通過顯式調用構造函數初始化。

    這裏說一下比較少用的一個地方,基類的公有成員經過私有繼承之後,變爲派生類的私有成員,外部不可訪問,但是可以使用using關鍵詞聲明,公有的成員經過繼承後,仍然是公有的,外部可以訪問。如以下代碼:只包含函數名就可以了,不需要額外的參數和返回值等。

using std::baseClass::min;

    如果在繼承的時候不明確聲明繼承方式,則默認是隱式聲明。

9.多重繼承:

        多重繼承是指,一個類繼承多個基類,有時候我們常常會遇到這樣的實際問題,但是在使用的過程中,常常會遇到一個問題,就是菱形繼承問題,也就是有基類A,然後子類B,C都是繼承A的,基類A中有個work函數,那麼B,C都重寫了這個work,然後超級子類D又多重繼承了B,C,那麼盲目使用就會出現問題,如A *pa = new D();這樣使用就出現問題了,實際上D中有兩個work函數,分別是繼承B和繼承C的,如果自己在重寫,那麼就相當於有三個work函數,基類實在無法確定這個是哪個work函數,那麼這樣使用就產生了二義性。

        爲了解決這個問題,C++提出了虛基類這個概念,也就是在繼承的時候增加virtual關鍵字,如:virtual public A,這樣就只含有一個基類的work,而並非多個。

            如果我們在上面不是用多態的性質,而且使用虛基類,而是直接創建D類對象,那麼在調用work函數的時候,依然會出現二義性,此時的解決方案是在函數前加一個類解析符,如B.work();上面說的兩個問題看似都是二義性的問題,但是一個是使用多態的時候面臨的二義性,而另一個是在不用多態性,而直接調用函數的二義性。


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