C++學習筆記——繼承與多態


繼承與多態都是C++中很重要的兩個概念,也是相比起C語言的優勢所在,下面就來總結一下C++中繼承與多態的概念以及簡單用法!

一.繼承

1.1 什麼是繼承?

面向對象思想是一種更加符合人類思維的思想,一開始我還不怎麼理解這句話,但是隨着對C++以及面向對象思想的學習,我越發感覺到這句話的意義所在。
因爲C++中的一大特點,封裝的存在,將一類對象的共同特點,共有的成員和方法封裝成類,但是,不可能每一類對象都要從頭進行封裝,就像遊戲給一個英雄添加了一項新技能技能,總不能重頭在創建一個新的英雄類吧,而且,在升級系統時有一個原則,那就是保持源碼的完整性,因爲源碼是經過很多測試的,如果修改,則需要重新測試,所以,我們可以採用繼承的方式,繼承原有的類,並在此基礎上新增一些自己特有的成員與方法,稱被繼承的類爲基類(父類),繼承下來的類叫派生類(子類)。

1.2 通過圖來理解繼承

定義一個基類:Person類

class Person  //Person類
{
    public:
        Person();  //默認構造
        char* GetName();
        void SetName(char *name);
        virtual ~Person();  //默認析構

    protected:

    private:
        char *m_name;
        int   m_age;
        bool  m_gender;
};

定義還一個Person類作爲基類後,在定義一個學生類,一個教師類來繼承Person類如下圖:
在這裏插入圖片描述
從圖中可知。Student類,Teacher類繼承了Person類,我只把派生類中的新成員與新方法寫了出來,但實際上,子類繼承了父類的雖有成員與方法。值得注意的是,子類雖然繼承了父類的私有成員,但卻不能直接訪問,須通過父類繼承下來的方法來獲取;
在子類繼承了父類的雖有成員與方法後,子類還新增了學號專業,工號部門等成員,新增了Get,Set方法;代碼實現:

class Student : public Person  //學生類
{
    public:
        Student();
        char* GetSNO();
        void SetSNO(char *sno);
        virtual ~Student();

    protected:

    private:
        char *m_sno;
        char *m_major;
};

class Teacher : public Person  //教師類
{
    public:
        Teacher();
        char* GetTNO();
        void SetTNO(char *tno);
        virtual ~Teacher();

    protected:

    private:
        char *m_tno;
        char *m_depart;
};

1.3 派生類構造函數的寫法

派生類雖然繼承了基類,但卻需要自己的構造函數與析構函數,因爲子類中有特有的成員,光靠父類的構造函數是無法進行初始化的,派生類的析構函數有下面幾種寫法:

  1. 使用基類的默認構造
Student(形參列表):
{
    //派生類中新成員的初始化;
}

這樣,程序會先調用基類的默認構造函數,在調用子類的構造函數;

  1. 調用基類重載的構造函數
Teacher(形參列表):基類名(實參列表)
{
    //派生類中的成員初始化;
}

編譯器會根據實參列表選擇基類相應的構造函數進行調用,其實參來自於Teacher的形參列表;

無論哪一種方法,編譯器都會先執行基類的構造函數分,在調用派生類的構造函數,而析構函數的調用則與構造函數調用順序相反。

1.4 多繼承中的二義性

派生類可以繼承一個基類,也可以繼承多個基類,我們管這中方式爲多繼承:
在這裏插入圖片描述
這樣,助教類就擁有了學生類與教師類的成員與方法:

class Assistant : public Student,public Teacher
{
    public:
        Assistant();
        virtual ~Assistant();

    protected:

    private:
};

這樣,我們在初始化Assistant對象時,就會有以下步驟:

Person構造 —> Student構造 —> Person構造 —> Teacher構造 —> Assistant構造
可知Person構造構造了兩次,若我們在assistant對象中,調用Person中的方法,assistant.GetName(); 那編譯器如何知道,我們調用的是Student中繼承而來的,還是Teacher中繼承而來的呢?所以編譯器會報錯。
多繼承的二義性,有以下兩種解決方法:

  1. 添加作用域
assistant.Student::GetName();
  1. 添加virtual關鍵字
    在定義派生類時,通過虛擬繼承的方式將基類聲明爲虛基類,虛基類中的成員在類的繼承中只會被繼承一次,從而解決了多繼承中的二義性問題,語法:
class 派生類名 : virtual 繼承方式 虛基類名
{
    .......
}

1.5 三種繼承方式

前面提到了共有繼承,也提到了基類的私有成員子類是無法訪問的,下面就詳細介紹繼承方式以及相關成員的訪問權限:
在這裏插入圖片描述
繼承方式:
在這裏插入圖片描述
也就是說,基類的私有成員子類無論使用那種那個繼承方式都不可以訪問,而採用保護繼承時,基類的共有與保護屬性的變量都會變爲保護屬性。

1.6 函數的重定義

由上面的Person類與Student類可知,當我們想打印Person類的信息時,只需打印姓名,年齡和性別,然而當子類繼承了這個方法後,子類有新增了學號,專業等信息,如果直接使用繼承得來的方法,則不能顯示完整的學生信息,只會顯示學生信息的姓名,年齡與性別,所以要重定義函數;
重定義函數與函數的重載不同,函數的重定義只能更改函數的內部實現,而不能修改函數頭。
例如,Person類的Show方法:

void Person::Show()
{
    cout << "個人信息" << endl;
    cout << "姓名: " << m_name << "\n";
    cout << "年齡: " << m_age << "\n";
    cout << "性別: " << m_gender << endl;  
}
void Student::Show()
{
    cout << "個人信息" << endl;
    cout << "姓名: " << m_name << "\n";
    cout << "年齡: " << m_age << "\n";
    cout << "性別: " << m_gender << "\n";
    cout << "學號: " << m_sno << "\n";
    cout << "專業: " << m_major << endl;
}

二. 多態

2.1 什麼是多態?

多態,向不同對象發送同一條消息(函數調用),不同對象在接收到消息後會產生不同的行爲,即不同的實現,執行不同的函數,或者函數名相同,但執行的細節相同。
打個比方,當我們進入召喚師峽谷後,大家都會收到一條消息,那就是“歡迎來到英雄聯盟!”,然而,不同的角色聽到這句話後,會有不同的動作,比如上單會往上路走,中單去中路,打野去反野或者刷自家Buff,下路雙人組幫助打野擊殺Buff一樣,不同對象在接收到相同的信息後的行爲不一樣,這就是多態。

2.2 類型兼容

所謂類型兼容,是C++已經爲我們實現好的一個功能,即

  1. 可以用派生類對象爲基類對象賦值:
Student student;
Person person = student;
  1. 可以用派生類的對象初始化基類的引用:
Teacher teacher;
Person& refPerson = teacher;
  1. 可以用派生類對象的地址爲基類類型的指針賦值:
Teacher teacher;
Person *person = &teacher;

這就是所謂的類型兼容。

2.3 虛函數

2.3.1 先期綁定

先期綁定,又稱靜態綁定,下面舉一個例子,假設我在父類中定義了一個方法:

void Print(Person& refperson)
{
    refperson.Show();
}

因爲前面提到的類型兼容的原因,在傳入參數時,我可以傳入Student的引用,也可以傳入Teacher類的引用,當然可以傳入Person類的引用,然而,當編譯器編譯後,便會將Show方法與Person類綁定,只要用到Show方法的地方,都會使用Person類的Show方法,即較爲低級的Show方法,並不能調用到新的會者說專屬的Show方法,是在編譯器編譯的時候就決定了,一種解決方法是使用函數重載:

void Print(Person& refperson);
void Print(Student& refstudent);
void Print(Teacher& refteacher);

在分別實現他們懂得內部細節即可,但這,都不是真正的多態,真正的多態,即“後期綁定”。

2.3.2 後期綁定

又叫動態綁定,即綁定發生於程序運行時,我們需要做的,只是在基類中定義函數時,加上一個
virtual 關鍵字,這樣,該函數則變爲虛函數:

virtual void Print(Person& refperson);virtual void Print(Person* ptrperson);

不可以是

virtual void Print(Person person);

這樣,在傳入不同的類後,編譯器會根據類的類型選擇相應的Show方法來執行,這邊是多態。

總結下來,要實現,需要下面3中要求:

  1. 類型兼容
  2. 虛函數
  3. 定義函數時使用的是指針或引用

2.4 虛函數的工作原理

假設,我們創建了這樣了以類:

class Person  //Person類
{
    public:
        Person();  //默認構造
        char* GetName();
        void SetName(char *name);
        virtual void Show();
        virtual void Work();
        virtual ~Person();  //默認析構

    protected:

    private:
        char *m_name;
        int   m_age;
        bool  m_gender;
};

當我們創建了一個Person類的實例後,其在內存中有一個隱藏指針,這個指針指向了一個叫虛函數表的東西:
在這裏插入圖片描述
當子類繼承基類後,同時繼承了虛函數表,當子類實現了Show方法,則該方法從表中移除。

三.抽象類

3.1 抽象類的定義

在面向對象的概念中,所有的對象都是通過類來描繪的,但是反過來,並不是所有的類都是用來描繪對象的,如果一個類中沒有包含足夠的信息來描繪一個具體的對象,這樣的類就是抽象類。
抽象類除了不能實例化對象之外,類的其它功能依然存在,成員變量、成員方法和構造方法的訪問方式和普通類一樣。
由於抽象類不能實例化對象,所以抽象類必須被繼承,才能被使用。也是因爲這個原因,通常在設計階段決定要不要設計抽象類。
父類包含了子類集合的常見的方法,但是由於父類本身是抽象的,所以不能使用這些方法。
打個比方:
在這裏插入圖片描述
基類是形狀類,這並不能實例化爲真正的對象,而只能通過其他類繼承後才能使用,因爲無法實例化成具體的對象,所以裏面的Draw(),Erase(),Move()都不能在本身使用。
一個類如果是抽象類,則該類中至少要有一個純虛函數:即初始化爲0的虛函數:

virtual <函數類型> <純虛函數名> (形參列表) = 0

這就是這一次的總結啦!之後會再補充一些其他的知識點!
順便慶祝大三下學期完結,從開學到考試全部在家中進行,
一個在家 上課+考試 的學期…

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