C++中的繼承與多態知識梳理

繼承

在面向對象編程語言中,都有三大特性:封裝,繼承和多態。
今天我們就來研究一下,C++中的繼承。

概念

繼承是在面向對象的編程中,把一些相同或者相近的屬性給抽象出來,以此達到代碼的複用功能,大大的提高了程序的開發效率。具體的講就是子類擁有了父類的所有成員。

通俗的講就是在寫程序時,有時候需要定義一個人類,好不容易寫完,寫全一個人的類,現在又要寫一個學生類,那麼沒有繼承就需要把人類有的屬性在學生類中寫一遍,再加上學生類特有的屬性。想一想,我們爲什麼不把人這個類複用起來呢?人類中的屬性學生類中也都有,所以我們可以用學生類來繼承人類,那麼人類就是學生類的父類也可以叫基類,子類就是學生類也叫派生類。
代碼舉例:

// 父類
class Person
{
    public:
    void fun()
    {
        cout << name <<endl;
    }
    protected:
    string name;
};
// 子類
class Student : public Person // 繼承關係
{
    private:
    string num;
};
  • 繼承方式
    重點來了。
    繼承中分爲三種繼承方式公有繼承(public)、保護繼承(protected)、私有繼承(private)。

繼承之間有什麼區別呢?
公有繼承:是訪問權限在父類中是什麼權限在子類中也是什麼權限,是繼承過來了父類的成員變量和成員函數,而沒有改變權限。(權限稍後解釋)

保護繼承:保護繼承就是在父類中的比保護權限大的成員變量和成員函數都變成保護權限。而比保護權限小的不變。怎麼理解呢?
父類中public修飾的成員保護繼承後也將變成保護權限,而父類中保護成員到子類中則還是protected,私有成員被繼承後,在子類中是不可見成員,但在子類中確實存在。

私有繼承:私有繼承就是把父類的成員繼承過來後,父類成員變成了私有屬性,只能被類的內部訪問,不能被外部訪問。父類中私有成員變爲不可見。

我麼來總體總結爲一副圖:
繼承關係圖

訪問限定符(權限是我自己起的,便於理解!哈哈!)
訪問限定符有三種:公有,保護,私有。你沒有看錯,和繼承方式中 三個一樣。
在這裏就說說各自的功能,公有限定符,可以被類外的對像直接訪問,保護限定符,可以被繼承的類進行訪問,但是不能被類外訪問。私有限定符,只能在類的內部進行訪問,被繼承後變爲不見。

賦值兼容性規則

在公有繼承中會出現把子類對象賦值給父類,這樣的做法是完全可以的。我們可以稱爲切割。
1)不能把父類對象賦值給子類對象。
2)能把子類對象賦值給父類對象。(因爲在賦值時候會形成臨時變量,而臨時變量具有常性,所以會切割一份和父類一樣的,賦值給父類)
我們用一張圖理解。
賦值關係
在上圖中,父類就是成員函數和成員變量一定包含在子類中,而子類中的成員函數和成員變量不一定全都在父類中,所以就子類可以賦值給父類,而父類不能賦值給子類。
還有兩點:
1)父類的指針或者引用可以指向子類
2)而子類的指針或者引用不能指向父類(非要指向可以強轉,但後果可能崩掉)

隱藏

隱藏是在繼承中,子類和父類有相同的名稱,當子類繼承父類後,父類中與子類相同命名的成員變量會隱藏起來。意思就是假設父子類中都有個變量a,如果在子類訪問,會直接訪問到子類中的那個成員變量a,而不是父類中的a這個成員變量。
如果要在子類中訪問父類的a,那麼必要要加上父類的域,例如:person::a;
對於函數而言:對於成員函數來說的,如果子類和父類都有同名的函數,那麼在繼承過程中,子類會對父類函數進行隱藏,如果要訪問父類中同名函數就要加上域名。
在這中可以定義同名變量或者同名函數是因爲每個類都有自己獨立的作用域。
這裏要注意:重載是在同一作用域而言,而重定義或者叫隱藏,實在不同的作用域中的。
關係重、載重定義、重寫三個不同後面比較解釋

子類(派生類)中的默認六個成員函數

在C++中的類中有六個默認成員函數,分別爲:

  1. 構造函數
  2. 拷貝構造函數
  3. 析構函數
  4. 賦值操作符重載
  5. 取地址操作符重載
  6. const修飾的取地址操作符重載

    在繼承中這六個默認函數是編譯系統自動合成的。
    換句話說就是子類繼承了父類那麼要怎麼寫子類的六個默認成員函數呢?
    我們這裏用代碼來體現:

// 父類
class Person
{
public:
    // 父類構造函數
    Person(const char* _name)
    :name(_name)
    {}

    // 父類的拷貝構造函數
    Person(const Person& n)
    :name(n.name)
    {}

    // 父類賦值運算符重載
    Person& operator=(const Person& p)
    {
        // 一定要先判斷是否是自己本身
        if (this != &p)
        {
            name = p.name;
        }
        return *this;
    }

    void fun()
    {
        cout << name <<endl;
    }
protected:
    string name;
};

// 子類
class Student : public Person // 繼承關係
{
public:
    // 子類構造函數
    Student(const char* _name, int _num)
    //在這裏直接調用了父類的構造函數,構造順序按照成員數據定義順序而構造
    //在子類中的初始化順序是先初始化父類成員再初始化子類中。
    :Person(_name),num(_num)
    {}

    // 子類的拷貝構造函數
    Student(const Student& s)
    // 這裏就採用了切割,可以把子類對象賦值給父類對象
    :Person(s),num(s.num) // 注意這裏必須用初始化列表
    {}

    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            // 這裏調用了父類中的賦值運算符重載
            Person::operator=(s);
            num = s.num;
        }
        return *this;
    }
private:
    int num;
};

還有兩個默認函數,一般用系統默認即可。

菱形繼承

單繼承與多繼承

在C++中支持多繼承,意思就是一個子類可以有多個父類。
單繼承就像上面Person類和Student類。
而多繼承我們舉個例子來說明。

// 父類1
class Teacher
{
    public:
    void fun()
    {
        cout << name <<endl;
    }
    protected:
    string name;
};

// 父類2
class Student 
{
    public:
    void function()
    {
        cout<<studnum<<endl;
    }
    private:
    string studnum
};

// 子類
class Assistant : public Teacher, public Student
{
    public:
    void function()
    {
        cout<<Assnum<<endl;
    }
    private:
    string Assnum;
};

如圖:多繼承

菱形繼承

在多繼承中雖然能集成各種的類型,但是有些類具有相同的成員變量,比如:
ling'xing
在這樣的繼承中Tearcher類和Student類繼承Person類,而Assistant類又繼承了Teacher和Student。當Person類中有一個name成員變量,被Teacher和Student繼承,又被Assistant繼承,那麼在Assistant中訪問name就不知訪問哪個父類中的name變量了。

菱形繼承帶來的問題
1)存在二義性
2)數據冗餘空間浪費

怎麼解決菱形繼承呢?

這裏我們就引出了一個關鍵字 virtual
如果出現了菱形繼承,那麼我們爲了解決上面的兩個問題,我們可以給中間繼承的兩個類加上virtual。這樣就形成了虛繼承的概念。
看代碼演示:

// 爺爺類(自己發明的!^_^)
class Person
{
    public:
    void fun()
    {
        cout << name <<endl;
    }
    protected:
    string name;
};

// 父類1
class Teacher : virtual public Person // 這裏就是虛繼承
{
    public:
    void fun1()
    {
        cout << Teachnum <<endl;
    }
    protected:
    string Teachnum;
};

// 父類2
class Student : virtual public Person // 虛繼承
{
    public:
    void function()
    {
        cout<<studnum<<endl;
    }
    private:
    string studnum
};

// 子類
class Assistant : public Teacher, public Student
{
    public:
    void function()
    {
        cout<<Assnum<<endl;
    }
    private:
    string Assnum;
};

在上上面的菱形繼承中,Person類的name成員變量被Assistant類繼承了兩次,所以裏面就會有兩個name,那麼我們加上virtual關鍵字後,結構就會改變。
咱們可以這樣理解:
繼承
虛繼承
上面這裏是在vs中的虛繼承存出,是用偏移量進行記錄name成員變量。

注意:雖然菱形繼承可以用虛繼承來處理,但是虛繼承在性能開銷也是比較大的。所以儘可能不要有菱形繼承。

還有一個重點,實現一個不能被繼承的類

在實現一個類中,如果要不被繼承那麼我們就可以在該類的構造函數中將它私有,但是又會有問題,如果私有後,那麼該類自己也不能在類外進行創建對象,私有成員不能在類外被訪問,但是可以在類裏面訪問,所以我們可以在類裏面提供公有的函數來進行創建對象,但是類裏的成員函數是屬於對象的,而我們要通過該成員函數進行創建對象,這樣就很矛盾。那麼就要定義成靜態成員函數,這樣該函數就不屬於對象,而是本類,可以通過類名來調用。那麼總結一下就是:

  1. 不能被繼承的類的構造函數一定要是私有的。
  2. 要同過靜態的成員函數進行調用構造函數實例化對象

具體我們用代碼實現一下:

class Demo
{
public:
    // 用靜態函數輔助構造函數
    static Demo* GetDemo(int a)
    {
        // 這個是在堆上開闢的
        return new Demo(a);
    }

    static Demo GetDemo(int a)
    {
        // 這是在棧上開闢的,並且是一個匿名對象
        return Demo(a);
    }
private:
    // 將構造函數進行私有化,被子類繼承爲不可見
    Demo(int _a):a(_a){}
    int a;  
};

繼承中的注意點

1)繼承中,基類的友元是不能被派生類繼承的,也就是說,父類的友元不能訪問子類中的保護和私有。
2)靜態static,在繼承中父類定義了靜態成員,那麼在整個繼承中只有一個這樣的成員,無論派生出多少子類,都只有一個static成員實例。

多態

在瞭解多態前我們先要了解一下虛函數。

什麼是虛函數。

虛函數–在類的成員函數前加上關鍵字virtual就形成了虛函數。(要注意這裏的虛函數與上面的虛繼承沒有一點關係,這個要分清)
爲什麼會有虛函數這樣的函數存在呢?這個就是爲了後面的多態。
虛函數還有一個特點虛函數重寫:繼承中父類中虛函數與子類中的虛函數相同,什麼叫相同,就是函數名,返回值,參數,和修飾詞virtual都要一樣,但是函數體內容可以不一樣,這樣就叫子類虛函數對父類虛函數的重寫也可以叫覆蓋。

注意:有虛函數的類中,會多開闢一個指針的字節,去指向虛表。虛表就是因爲虛函數的重寫而有的存儲方式。這樣可以減少類的每個對象多開闢出額外的空間。

我們來總結一下虛函數:

  1. 派生類重寫基類中的虛函數,需要函數名,返回值,參數都相同(除過協變)協變:就是基類的返回值是基類的指針或者引用,派生類返回值是派生類的指針或者引用,也構成重寫。
  2. 如果在基類中定義了虛函數,那麼在派生類中也保持虛函數的特性。
  3. 只有類的成員函數才能定義成虛函數。
  4. 靜態成員函數不能定義成虛函數。
  5. 如果在類外定義虛函數,虛函數關鍵字必須在聲明上,而類外的虛函數不需要加關鍵字。
  6. 構造函數不能爲虛函數,operator=可以成虛函數,但是建議不要虛函數,容易產生混淆。
  7. 不要在構造函數和虛構函數中調用虛函數,因爲在構造函數中對象還沒有定義出來,容易發生未定義行爲。
  8. 如果在繼承中,最好把父類的析構函數定義成虛函數。這樣可以構成多態,析構的時候是按照對象進行析構,如果定義成person* a = new Student;這樣的話就會在虛表中查找相應的析構函數。如果不構成多態,那麼會按照類型調用析構,上面的就會去調用父類的析構函數,但是new出來的對象是子類,會造成內存泄漏。

多態

有了虛函數那麼我們再來看多態。
多態是在繼承中一個函數有多種形態。先說滿足多態的條件

1. 必須在繼承中,存在虛函數,並且有虛函數重寫
2. 必須是父類的指針或者引用調用重寫的虛函數。

多態就是一個函數有多種形態,換句話就是當父類的指針或者引用指向父類時就調用的是父類中重寫的虛函數,當指向子類時就調用的是子類中重寫的虛函數。

舉例子說明:

// 父類
class Person
{
    public:
    virtual void function()
    {
        cout << "父類" <<endl;
    }
    protected:
    string name;
};
// 子類
class Student : public Person // 繼承關係
{
    virtual void function()
    {
        cout << "子類" <<endl;
    }
    private:
    string num;
};
// 調用的函數
void fun(Person& p)
{
    p.function();
}

int main()
{
    Person p;
    Student s;
    fun(p);
    fun(s);
}

運行結果:
運行結果
這樣就形成了多態。形成多態後,函數參數只與對象有關,與類型無關,傳過去的是子類對象就調用子類重寫的虛函數,傳父類對象就調用父類重寫的虛函數。

純虛函數

什麼是純虛函數,在成員虛函數的形參後面加上=0,成員函數就爲純虛函數。
包含純虛函數的類叫抽象類(接口類),抽象類不能實例出對象。純虛函數在子類中重寫了純虛函數,才能實例出對象。如果不重寫純虛函數,那麼子類也會變成抽象類,實例不出對象。
代碼體現:

class A
{
    public:
    // 純虛函數
    virtual void fun() = 0;
    protected:
    int a;
};

什麼時候定義成純虛函數?當想讓子類必須重寫你的純虛函時,可以定義成純虛函數。

總結一下重定義(隱藏)、重載、重寫(覆蓋)的區別

區別

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