【C++】歸納要點,輕鬆學會繼承

從前一座大山下住着一名老翁,他家門前有兩座大山,切斷了他家和外界的聯繫。因此他決心把山平掉,另一個“聰明”的智叟笑他太傻, 認爲不能。老翁說:“汝心之固,固不可徹,曾不若孀妻弱子。雖我之死,有子存焉;子又生孫,孫又生子;子又有子,子又有孫;子子孫孫無窮匱也,而山不加增,何苦而不平?”

大家都知道是誰吧,當初看到繼承這個概念,我第一反應就是愚公的那句,“雖我之死,有子存焉;子又生孫,孫又生子;子又有子,子又有孫;子子孫孫無窮匱也”。它的理想信念今生完不成,繼承給他兒子,兒子完不成再繼承給孫子,這樣繼承下去,早晚有一天山會移完的。

故事聽到這裏也該回到正題了,通過這個故事,我想說的是我最早從課本上生動的瞭解到的關於繼承的概念就是來自它了。而今天,作爲計算機專業學生,就讓我向你們講述什麼是C++編程語言的繼承。Let’s go~~~


第一步·初識繼承

  1. 繼承是什麼
    面向對象程序設計使代碼可以複用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能。這樣產生新的類,稱派生類。

  2. 定義格式:
    class 派生類名 : 繼承方式 基類名
    {
    派生類新增的數據成員和成員函數;
    }

  3. 繼承關係&訪問限定符
    3種類成員訪問限定符: public(公有),private(私有),protected(保護)

    3種繼承關係:public(公有繼承),private(私有繼承),protected(保護繼承)

  4. 繼承權限
    這裏寫圖片描述
    (1)公有繼承
    ①基類的public和protected成員的訪問屬性在派生類中保持不變,但基類的private成員在派生類中存在不可訪問。
    ②派生類的成員函數可直接訪問基類的public和protected成員,但不能訪問基類的private成員。
    ③通過派生類的對象只能訪問基類的public成員。
    (2)私有繼承
    ①基類的public和protected成員都以private身份出現在派生類中,但基類的private成員在派生類中存在不可訪問 。
    ②派生類中的成員函數可以直接訪問基類中的public和protected成員,但不能訪問基類的private成員。
    ③通過派生類的對象補不能訪問基類中的任何成員。
    (3)保護繼承
    ①基類的public和protected成員都以protected身份出現在派生類中,但基類的private成員在派生類中存在不可訪問。
    ②派生類中的成員函數可以直接訪問基類中的public和protected成員,但不能訪問基類的private成員。
    ③通過派生類的對象不能訪問基類中的任何成員。

  5. 總結
    (1)私有成員繼承下來了,在派生類中存在但不能訪問。基類的私有成員無論以哪一種繼承方式,在派生類中都是不可訪問的。其本質可解釋爲作用域變了,基類和派生類是兩個不同的作用域,而私有成員在類外是不能夠被訪問的。

    (2)在類不參與繼承時,private和protected成員的屬性都可以被認爲是私有的。但當類參與繼承時,基類的private成員不論哪種繼承,在派生類中是不能被訪問的,在類外更不可能通過派生類對象訪問;基類的protected成員不論哪種繼承,在派生類中都可以被訪問,但在類外不能通過派生類對象訪問。可以看出保護成員限定符是因繼承纔出現的,這就是private與protected的不同。這樣,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義爲protected。

    (3)使用關鍵字class時默認的繼承方式是private,使用關鍵字struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。

    (4)同名隱藏。當基類的成員或成員函數和派生類的成員或成員函數同名,通過派生類的對象訪問成員或成員函數時,優先訪問派生類自己的。此時若想訪問基類的,只需在前面加上作用域限定符。
    例:
    Derived d;
    d._pub = 2;//訪問派生類自己的
    d.Base::_pub = 1;//訪問基類的
    !!要注意的是。當派生類的成員函數與基類的成員函數同名時,此時並不構成重載,因爲不在同一作用域。通過派生類的對象訪問成員函數時,優先訪問派生類自己的。而不是按照有無參數,有無返回值去找適合它的那個成員函數。
    例:

class Base
{
public:
    Fun(int x);
    int a;
}

class Derived:public Base
{
public:
    Fun();
    int b;
}

int main()
{
    Derived d;
    d.Fun(25); 
    //此處出錯,因爲優先找派生類的Fun()發現不合適,報錯,它只會優先 調用派生類自己的,而不是調適合自己的

    return 0;
}

第二步· 繼承相關要點

1.對象模型
指的是對象中各個成員變量的分佈格式。
以簡單的派生類對象模型爲例,見如下代碼。

class Base
{
public:
     int _b;
};

class Derived:public Base
{
public:
     int _d;
};

int main()
{
     Derived d;
     d._b = 10;
     d._d = 11;

     return 0;
}

這裏寫圖片描述

其他的對象模型我們在後面還會提到。

2.派生類中構造函數/析構函數的調用

先給出結論。
派生類中構造函數的調用順序:
派生類構造函數——>基類構造函數

派生類中構造函數的執行順序:
基類構造函數——>派生類構造函數

派生類中析構函數的調用順序:
派生類析構函數——>基類析構函數

如下代碼爲例:

class Base1
{
public:
     Base1(int data):_data(data)
     { cout<<"Base1()"<<endl; }
     ~ Base1( ){ cout<<"~Base1()"<<endl; }
protected:
     int _data;
};

class Base2
{
public:
     Base2(int data):_data2(data)
     { cout<<"Base2()"<<endl; }
     ~ Base2( ){ cout<<"~Base2()"<<endl; }
protected:
     int _data2;
} ;

class Derive:public Base1,public Base2
{
public:
     Derive():Base1(0),Base2(1),_d(3)
     { cout<<"Derive()"<<endl; }
     ~ Derive( ){ cout<<"~Derive()"<<endl ; }
protected:
     int _d;
};

int main()
{
     Derive d;
     return 0;
}

這裏寫圖片描述
通過結果,驗證了我們之前的結論。

說明:
之所以派生類中構造函數的調用順序和執行順序不同。是因爲在調用派生類構造函數時,在派生類初始化列表的位置要先調用基類構造函數,完成對基類構造函數的初始化,完成後再接着執行派生類構造函數。所以它是先調用派生類,後調用基類;但卻是先執行基類的,再執行派生類的。

派生類中含有子對象時構造函數/析構函數的調用
先給出結論。
派生類中含有子對象時構造函數的執行順序:
基類構造函數——>派生類子對象構造函數——>派生類構造函數

派生類中含有子對象時析構函數的調用順序:
派生類析構函數 ——>派生類子對象析構函數——>基類析構函數
用以下代碼來驗證:

class Base1
{
public:
     Base1(int data):_data(data)
     { cout<<"Base1()"<<endl; }
     ~ Base1( ){ cout<<"~Base1()"<<endl; }
protected:
     int _data;
};

class Derive:public Base1
{
public:
     Derive():Base1(0),b1(1)
     { cout<<"Derive()"<<endl; }
     ~ Derive( ){ cout<<"~Derive()"<<endl ; }
protected:
     Base1 b1;
};

int main()
{
     Derive d;
     return 0;
}

這裏寫圖片描述
通過結果,驗證了我們之前的結論。

知識點總結:
①派生類初始化列表中的順序是按照派生類對象模型中的順序進行的。(自上而下)
②構造函數執行順序在對象模型中是自上而下;析構函數調用順序在對象模型中是自下而上。
③基類和派生類是兩個不同的作用域。
④派生類不能繼承基類的構造函數和析構函數。
⑤基類沒有定義構造函數,則派生類也可以不用定義,全部使用缺省構造函數。
說明⑤:
當基類沒有構造函數,則派生類也可以不用定義,全部由系統默認提供,系統提供的是缺省構造函數。就是以下這種。
Derived( )
:Base( )
{ }
⑥基類定義了帶參構造函數,派生類就一定要自定義構造函數。
說明⑥:
但當基類有帶參構造函數時,派生類就一定得自定義構造函數。如此時派生類不自定義構造函數,系統就會提供一個默認構造函數,如上。
此時系統所提供的構造函數在初始化列表中調用的是Base( ),而不是調用基類的帶參構造函數Base(int x)。因爲系統還無法智能到調用構造函數時,還給出參數值。若基類此時沒有Base( ),就會出錯,提示Base( )找不到。
⑦基類沒有缺省構造函數(無參和全缺省的構造函數都沒有,則派生類必須要在初始化列表中顯式給出基類名和參數列表。

由上可知,當基類有缺省構造函數。派生類沒有構造函數時,系統就會爲派生類合成一個默認的構造函數。

3.賦值兼容規則
①子類對象可以賦值給父類對象
②父類對象不能賦值給子類對象
③父類的指針或引用可以指向子類的對象
④子類的指針或引用可以指向父類的對象
⑤如果函數的形參是基類的對象或引用,在調用函數時可以用派生類對象作爲實參

4.友元、靜態成員和繼承
(1)友元不是類的成員函數,故友元函數是不可以被繼承的。
(2)靜態成員是類的屬性,所以是可繼承的。但無論被繼承幾次,這些繼承下來的靜態成員都只有一個實體。

第三步· 單繼承&多繼承&菱形繼承

單繼承:一個子類只有一個直接父類時稱這個繼承關係爲單繼承。
單繼承比較簡單,對象模型之前已經分析了,這裏不再多說。

多繼承:一個子類有兩個或以上直接父類時稱這個繼承關係爲多繼承。
這裏寫圖片描述
多繼承裏最經典的就是菱形繼承了。我們來看一下。
這裏寫圖片描述

class B
{
public:
     int _b;
};

class C1:public B
{
public:
     int _c1;
};

class C2:public B
{
public:
     int _c2;
};

class D:public C1,public C2
{
public:
     int _d;
};

int main()
{
     D d;
     d._b = 1;//錯誤,error C2385: 對“_b”的訪問不明確
     return 0;
}

出現對“_b”的訪問不明確的原因是因爲,類C1和C2中都繼承了來自B的_b,而D又繼承於C1和C2,所以通過D的對象去訪問_b時,不知道
訪問的是誰的_b,出現了二義性問題。其中,D的對象模型如下:
這裏寫圖片描述

爲了解決多繼承中的二義性問題,我們引入虛擬繼承
這裏寫圖片描述
利用虛擬繼承以後的代碼如下:

class B
{
public:
     int _b;
};

class C1:virtual public B
{
public:
     int _c1;
};

class C2:virtual public B
{
public:
     int _c2;
};

class D:public C1,public C2
{
public:
     int _d;
};

int main()
{
     D d;
     d._b = 1;//解決了二義性問題
     d._c1 = 2;
     d._c2 = 3;
     d._d = 4;
     cout<<sizeof(d)<<endl;//大小等於24,分析爲什麼?
     return 0;
}

從內存上看一下,果然是24字節。
這裏寫圖片描述
這樣我們可以得到D的對象模型,可以發現虛擬繼承時,D的對象模型發生了改變。
這裏寫圖片描述

說明:
(1)C1類和C2類都繼承了來自B類的成員_b,但C1和C2要訪問_b時,只能通過自己的偏移表指針找到偏移表,拿到相對於_b的偏移量,才能訪問到_b,所以C1和C2的大小是12字節。
(2)_b在D類中最終只保留了一份。D類可以直接訪問_b.

總結:
①虛擬繼承時派生類對象模型如下
這裏寫圖片描述
②把偏移表指針放在派生類對象模型的前4個字節
③在編譯時,偏移表就已形成,在調用構造函數時把偏移表指針放在派生類對象模型的前4個字節。
④編譯器爲了區分是普通繼承還是虛擬繼承,在虛擬繼承時先將1壓棧,在普通繼承時先將0壓棧。
⑤一定要注意virtual關鍵字放的位置,放的位置不同,表示的含義不同。
示例代碼:

class B
{
public:
     int _b;
};

class C1: public B
{
public:
     int _c1;
};

class C2: public B
{
public:
     int _c2;
};

class D:virtual public C1,virtual public C2
{
public:
     int _d;
};

int main()
{
     D d;
     //d._b = 1;//錯誤,error C2385: 對“_b”的訪問不明確,沒有解決二義性問題
     d._c1 = 1;
     d.C1::_b = 2;
     d._c2 = 3;
     d.C2::_b = 4;
     d._d = 5;
     cout<<sizeof(d)<<endl;//大小爲24,分析爲什麼?
     return 0;
}

查看內存分佈如下:
這裏寫圖片描述
所以我們可以得到D的對象模型如下:
這裏寫圖片描述
這樣一來,隨着virtual位置的不同,表達的含義就變了,所以一定要注意。

以上就是我關於繼承部分的歸納總結,若有什麼錯誤,一定要告訴我~~~~

發佈了31 篇原創文章 · 獲贊 19 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章