文章目錄
繼承與多態都是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 派生類構造函數的寫法
派生類雖然繼承了基類,但卻需要自己的構造函數與析構函數,因爲子類中有特有的成員,光靠父類的構造函數是無法進行初始化的,派生類的析構函數有下面幾種寫法:
- 使用基類的默認構造
Student(形參列表):
{
//派生類中新成員的初始化;
}
這樣,程序會先調用基類的默認構造函數,在調用子類的構造函數;
- 調用基類重載的構造函數
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中繼承而來的呢?所以編譯器會報錯。
多繼承的二義性,有以下兩種解決方法:
- 添加作用域
assistant.Student::GetName();
- 添加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++已經爲我們實現好的一個功能,即
- 可以用派生類對象爲基類對象賦值:
Student student;
Person person = student;
- 可以用派生類的對象初始化基類的引用:
Teacher teacher;
Person& refPerson = teacher;
- 可以用派生類對象的地址爲基類類型的指針賦值:
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中要求:
- 類型兼容
- 虛函數
- 定義函數時使用的是指針或引用
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;
這就是這一次的總結啦!之後會再補充一些其他的知識點!
順便慶祝大三下學期完結,從開學到考試全部在家中進行,
一個在家 上課+考試 的學期…