C++基礎-繼承

本文爲 C++ 學習筆記,參考《Sams Teach Yourself C++ in One Hour a Day》第 8 版、《C++ Primer》第 5 版、《代碼大全》第 2 版。ios

繼承是一種複用,不一樣抽象層次的對象能夠複用相同的特性。繼承一般用於說明一個類(派生類)是另外一個類(基類)的特例。繼承的目的在於,經過「定義能爲兩個或更多個派生類提供共有元素的基類」的方式寫出更精簡的代碼。函數

1. 繼承基礎

本節以公有繼承爲例,說明繼承中的基礎知識。學習

平常生活中的繼承示例:spa

基類 派生類
Fish(魚) Goldfish(金魚)、 Carp(鯉魚)、 Tuna(金槍魚,金槍魚是一種魚)
Mammal(哺乳動物) Human(人)、 Elephant(大象)、 Lion(獅子)、 Platypus(鴨嘴獸,鴨嘴獸是一種哺乳動物)
Bird(鳥) Crow(烏鴉)、 Parrot(鸚鵡)、 Ostrich(鴕鳥)、 Platypus(鴨嘴獸,鴨嘴獸也是一種鳥)
Shape(形狀) Circle(圓)、 Polygon(多邊形,多邊形是一種形狀)
Polygon(多邊形) Triangle(三角形)、 Octagon(八角形,八角形是一種多邊形,而多邊形是一種形狀)

1.1 繼承與派生

基類(好比魚類)派生出派生類(好比金槍魚類),派生類繼承基類。公有繼承中,派生類是基類的一種,好比,咱們能夠說,金槍魚是魚的一種。指針

閱讀介紹繼承的文獻時,「從…繼承而來」(inherits from)和「從…派生而來」(derives from)術語的含義相同。一樣,基類(base class)也被稱爲超類(super class);從基類派生而來的類稱爲派生類(derived class),也叫子類(sub class)。調試

1.2 構造函數的繼承與覆蓋

一個類只初始化其直接基類,出於一樣的緣由,一個類也只繼承其直接基類的構造函數。code

派生類繼承直接基類的構造函數的方法是使用 using 聲明語句,以下:對象

class Base
{
public:
    Base() {};                         // 1. 默認、拷貝、移動構造函數不能被繼承和覆蓋
    Base(int a) {};                    // 2. 被派生類中的構造函數覆蓋
    Base(int a, int b) {};             // 3. 被派生類中的構造函數繼承
    Base(int a, string b) {};          // 3. 被派生類中的構造函數繼承
};

Class Derived: public Base
{
public:
    using Base::Base;                  // 繼承基類中的構造函數
    Derived(int a) {};                 // 覆蓋基類中的構造函數
};

一般狀況下,using 聲明只是令某個名字在當前做用域可見。而看成用於構造函數時,using 聲明語句將令編譯器生成代碼。對於基類的每一個構造函數,編譯器都在派生類中生成一個形參列表徹底至關的構造函數。不過有兩種例外狀況,第一種:若是派生類構造函數與基類構造函數參數表同樣,則至關於派生類構造函數覆蓋了基類構造函數,這種狀況被覆蓋的基類構造函數沒法被繼承;第二種:默認、拷貝、移動構造函數不會被繼承。根據這些規則,上例代碼由編譯器生成的派生類構造函數形式以下:繼承

Class Derived: public Base
{
public:
    Derived(int a, int b) : Base(a, b) {};
    Derived(int a, string b) : Base(a, b) {};
};

1.3 派生類調用基類構造函數

派生類調用基類構造函數有三種形式:內存

  1. 若是基類有默認構造函數,派生類構造函數會隱式調用基類默認構造函數,這由編譯器實現,不需編寫調用代碼;
  2. 若是基類沒有默認構造函數,即基類提供了重載的構造函數,則派生類構造函數經過初始化列表來調用基類構造函數,這屬於顯式調用。這種方式是必需的,不然編譯器會試圖調用基類默認構造函數,而基類並沒有默認構造函數,編譯會出錯;
  3. 在派生類構造函數中,使用 ::Base() 形式顯示調用基類構造函數。和基類普通函數的調用方式不一樣,派生類中調用基類普通函數的形式爲 Base::Function()(須要指定類名)。雖然這種方式和第 2 種方式實現功能基本同樣,但若是隻使用這種方式而缺乏第 2 種方式,編譯會出錯。這種方式彷佛沒有什麼意義。

若是基類包含重載的構造函數,須要在實例化時給它提供實參,則建立派生類對象時,可使用初始化列表,並經過派生類的構造函數調用合適的基類構造函數。

class Base
{
public:
    Base(int a) { m = a };
private:
    int m;
};

Class Derived: public Base
{
public:
    Derived(): Base(25) {};               // 基類構造函數被調用一次,最終 Base::m 值爲 25
    Derived(): Base(25) { ::Base(36) };   // 基類構造函數被調用兩次,最終 Base::m 值爲 25
    Derived() { ::Base(36) };             // 編譯器試圖調用基類默認構造函數 Base::Base(),編譯出錯
};

1.4 構造順序與析構順序

Tuna 繼承自 Fish,則建立Tuna對象時的構造順序爲:1. 先構造 Tuna 中的 Fish 部分;2. 再構造 Tuna 中的 Tuna 部分。實例化 Fish 部分和 Tuna 部分時,先實例化成員屬性,再調用構造函數。析構順序與構造順序相反。示例程序以下:

#include <iostream>
using namespace std; 

class FishDummyMember
{
public:
    FishDummyMember() { cout << "FishDummyMember constructor" << endl; }
    ~FishDummyMember() { cout << "FishDummyMember destructor" << endl; }
};

class FishPrivateMember
{
public:
    FishPrivateMember() { cout << "FishPrivateMember constructor" << endl; }
    ~FishPrivateMember() { cout << "FishPrivateMember destructor" << endl; }
};

class Fish
{
protected:
    FishDummyMember dummy;

private:
    FishPrivateMember dummy2;

public:
    Fish() { cout << "Fish constructor" << endl; }
    ~Fish() { cout << "Fish destructor" << endl; }
};

class TunaDummyMember
{
public:
    TunaDummyMember() { cout << "TunaDummyMember constructor" << endl; }
    ~TunaDummyMember() { cout << "TunaDummyMember destructor" << endl; }
};

class Tuna: public Fish
{
private:
    TunaDummyMember dummy;

public:
    Tuna() { cout << "Tuna constructor" << endl; }
    ~Tuna() { cout << "Tuna destructor" << endl; }
};
   
int main()
{
    Tuna tuna;
}

爲了幫助理解成員變量是如何被實例化和銷燬的,定義了兩個毫無用途的空類:FishDummyMember 和 TunaDummyMember。程序輸出以下:(//後不是打印內容,是說明語句)

FishDummyMember constructor     // 基類數據成員實例化
FishPrivateMember constructor   // 基類數據成員實例化
Fish constructor                // 基類構造函數
TunaDummyMember constructor     // 派生類數據成員實例化
Tuna constructor                // 派生類構造函數
Tuna destructor                 // 派生類析構函數
TunaDummyMember destructor      // 派生類數據成員銷燬
Fish destructor                 // 基類析構函數
FishPrivateMember destructor    // 基類數據成員銷燬
FishDummyMember destructor      // 基類數據成員銷燬

注意,構建派生類對象時,基類的私有數據成員也會被實例化,只不過派生類沒有權限訪問基類的私有成員。參 3.1 節。

1.5 基類方法的覆蓋與隱藏

#include <iostream>
using namespace std; 

class Fish
{
private:
    bool isFreshWaterFish;

public:
    // Fish constructor
    Fish(bool IsFreshWater) : isFreshWaterFish(IsFreshWater){}

    // using Fish::Swim;         // 4.2 基類中全部 Swim() 方法不做隱藏

    void Swim()                  // 1.1 此方法被派生類中的方法覆蓋
    {
        if (isFreshWaterFish)
            cout << "[A] Fish swims in lake" << endl;
        else
            cout << "[A] Fish swims in sea" << endl;
    }

    void Swim(bool freshWater)   // 1.3 此方法被派生類中的方法隱藏
    {
        if (freshWater)
            cout << "[B] Fish swims in lake" << endl;
        else
            cout << "[B] Fish swims in sea" << endl;
    }

    void Fly()
    {
        cout << "Joke? A fish can fly? << endl;
    }
};

class Tuna: public Fish
{
public:
    Tuna(): Fish(false) {}

    void Swim()                  // 1.2 覆蓋派生類中的方法
    {
        cout << "Tuna swims real fast" << endl;
    }
};

class Carp: public Fish
{
public:
    Carp(): Fish(true) {}

    void Swim()                  // 1.2 覆蓋基類中的方法
    {
        cout << "Carp swims real slow" << endl;
        Fish::Swim();             // 3.2 在派生類中調用基類方法(繼承獲得)
        Fish::Fly();              // 5.2 在派生類中調用基類方法(繼承獲得)
    }
    
    /*
    void Swim(bool freshWater)   // 4.3 覆蓋基類中 Swim(bool) 方法
    {
        Fish::Swim(freshWater);
    }
    */
};

int main()
{
    Carp carp;
    Tuna tuna;

    carp.Swim();                 // 2.1 調用派生類中的覆蓋方法
    tuna.Swim();                 // 2.2 調用派生類中的覆蓋方法
    tuna.Fish::Swim();           // 3.1 調用基類中被覆蓋的方法
    tuna.Fish::Swim(false);     // 4.1 調用基類中被隱藏的方法
    tuna.Fly();                  // 5.1 調用基類中的其餘方法(繼承獲得)

    return 0;
}

方法的覆蓋與隱藏,參考註釋 1.1 1.2 1.3。
調用派生類中的覆蓋方法,參考註釋 2.1 2.2。
調用基類中被覆蓋的方法,參數註釋 3.1 3.2。
調用基類中被隱藏的方法,參數註釋 4.1 4.2 4.3。
調用基類中的其餘方法,參數註釋 5.1 5.2。

2. 訪問權限與類的繼承方式

訪問權限有三種:公有 (public)、保護 (protected) 和私有 (private),這三個關鍵字也稱訪問限定符。訪問限定符出如今兩種場合:一個是類的成員的訪問權限,類有公有成員、保護成員和私有成員;一個是類的繼承方式,繼承方式有公有繼承、保護繼承和私有繼承三種。

這兩種場合的訪問權限組合時,編譯器採用最嚴格的策略,確保派生類中繼承獲得的基類成員具備最低的訪問權限。例如,基類的公有成員遇到私有繼承時,就變成派生類中的私有成員;基類的保護成員遇到公有繼承時,就變成派生類中的保護成員;基類的私有成員派生類不可見。

注意一點,基類的私有成員派生類不可見,但派生類對象裏實際包含有基類的私有成員信息,只是它沒有權限訪問而已。參 3.1 節。

2.1 類成員訪問權限

類的成員有三種類型的訪問權限:

public: public 成員容許在類外部訪問。類外部訪問方式包括經過類的對象訪問,經過派生類的對象訪問以及在派生類內部訪問。

protected: protected 成員容許在類內部、派生類內部和友元類內部訪問,禁止在繼承層次結構外部訪問。

private: private 成員只能在類內部訪問。

類的內部包括類的聲明以及實現部分,類的外部包括對當前類的調用代碼以及其它類的聲明及實現代碼。

2.2 公有繼承

公有繼承的特色是基類的公有成員和保護成員做爲派生類的成員時,它們都保持原來的狀態。基類的公有成員在派生類中也是公有成員,基類的保護成員在派生類中也是保護成員,基類的私有成員派生類不可見。

公有繼承用於"是一種"(is-a)的關係。is-a 表示派生類是基類的一種,好比金槍魚(派生類)是魚(基類)的一種。

2.3 私有繼承

私有繼承的特色是基類的公有成員和保護成員都變成派生類的私有成員。基類的私有成員仍然爲基類所私有,派生類不可見。

私有繼承使得只有派生類才能使用基類的屬性和方法,所以表示「有一部分」(has-a)關係。has-a 表示基類是派生類的一部分,好比發動機(基類)是汽車(派生類)的一部分。

2.4 保護繼承

保護繼承的特色是基類的公有成員和保護成員都變成派生類的保護成員。基類的私有成員仍然爲基類所私有,派生類不可見。

與私有繼承相似,保護繼承也表示 has-a 關係。不一樣的時,基類的公有和保護成員變爲派生類中的保護成員,可以被派生類及派生類的子類訪問。

2.5 總結

下表中,表頭部分表示基類的三種成員,表格正文部分表示不一樣繼承方式下,對應的基類成員在派生類中的訪問權限。以表格第四行第二列爲列,表示在私有繼承方式下,基類的 public 成員將成爲派生類中的 private 成員。

基類成員 public 成員 protected 成員 private 成員
共有繼承 public 成員 protected 成員 不可見
保護繼承 protected 成員 protected 成員 不可見
私有繼承 private 成員 private 成員 不可見

3. 基類對象與派生類對象的賦值關係

3.1 派生類對象與基類的關係

#include <iostream>
using namespace std;

class Base
{
private:
    int x = 1;
    int y = 2;
    const static int z = 3;
};

class Derived : public Base
{
private:
    int u = 11;
    int v = 22;
    const static int w = 33;
};

int main()
{
    Base base;
    Derived derived;

    cout << "sizeof(Base) = " << sizeof(Base) << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;

    return 0;
}

程序輸出爲:

sizeof(Base) = 8
sizeof(Derived) = 16

類裏的 static 成員屬於整個類,而不屬於某一個對象,不計入類的 sizeof ( sizeof(類名) 等於 sizeof(對象名) ),所以 sizeof(Base) 值是 8。對於派生類 Derived,其 sizeof 運算結果爲基類數據成員佔用空間大小加上派生類數據成員佔用空間大小,所以值爲16。

注意一點,派生類對象所在的內存空間裏含有基類數據成員信息,包括基類私有數據成員,但派生類沒有權限訪問基類私有數據成員,編譯器在語法上不支持。

使用 gdb 調試程序,打印出基類對象和派生類對象的值,可獲得以下信息:

(gdb) p base
$1 = {x = 1, y = 2, static z = 3}
(gdb) p derived
$2 = {<Base> = {x = 1, y = 2, static z = 3}, u = 11, v = 22, static w = 33}

3.2 切除問題

將派生類對象複製給基類對象有以下兩種狀況:

第一種:經過賦值操做將派生類對象複製給基類對象

Derived objDerived;
Base objectBase = objDerived;

第二種:經過傳參方式將派生類對象複製給基類對象

void UseBase(Base input);
...
Derived objDerived;
UseBase(objDerived); // copy of objDerived will be sliced and sent

這兩種狀況下,編譯器都是隻複製派生類對象的基類部分,而不是複製整個對象。這種無心間裁減數據,致使 Derived 變成 Base 的行爲稱爲切除(slicing)。

要避免切除問題,不要按值傳遞參數,而應以指向基類的指針或 const 引用的方式傳遞。參《C++ 多態》筆記第 1 節。

3.3 賦值關係

以下三條關係的根本緣由在 3.1 節中已講述。

派生類對象能夠賦值給基類對象,反之則不行。
由於派生類對象數據成員比基類對象數據成員多。將派生類對象賦值給基類對象,基類對象可以獲得全部數據成員的值。反過來,將基類對象賦值給派生類對象,派生類對象中部分數據成員沒法取得合適的值,所以賦值失敗。

派生類指針能夠賦值給基類指針,反之則不行。
由於派生類指針所指向內存塊比基類指針所指向內存塊大。基類指針能夠指向派生類對象,取基類大小的內存便可。反過來,派生類指針若指向基類對象,勢必會形成內存越界。

派生類對象能夠賦值給基類引用,反之則不行。
由於派生類對象比基類對象空間大。將派生類對象賦值給基類引用,基類引用表示派生類對象中的基類部分,多餘部分捨棄便可。反過來,顯然不行。

以下:

Base     base;
Derived  derived;

         base     = derived;     // 正確
         derived  = base;        // 錯誤

Base    *pbase    = &derived;    // 正確
Derived *pderived = &base;       // 錯誤

Base    &rbase    = derived;     // 正確
Derived &rderived = base;        // 錯誤

4. 多繼承

派生類繼承多個基類的特徵稱做多繼承。如對鴨嘴獸來講。鴨嘴獸具有哺乳動物、鳥類和爬行動物的特徵,那麼鴨嘴獸能夠繼承哺乳動物、鳥類和爬行動物這三個基類。代碼形如:

class Platypus: public Mammal, public Reptile, public Bird
{
// ... platypus members
};

5. 禁止繼承

從 C++11 起,編譯器支持限定符 final。被聲明爲 final 的類不能用做基類,所以禁止繼承。

相關文章
相關標籤/搜索