北京家裝設(shè)計師排名北京網(wǎng)站優(yōu)化推廣方案
💓博主CSDN主頁:杭電碼農(nóng)-NEO💓
?
?專欄分類:C++從入門到精通?
?
🚚代碼倉庫:NEO的學習日記🚚
?
🌹關(guān)注我🫵帶你學習C++
? 🔝🔝
多態(tài)
- 1. 前言
- 2. 多態(tài)的概念以及定義
- 3. 多態(tài)的實例調(diào)用情況
- 4. 構(gòu)成多態(tài)的兩個特例
- 5. 多態(tài)的底層原理分析(一)
- 6. 多態(tài)底層原理分析(二)
- 7. 多態(tài)中的兩個關(guān)鍵字
- 8. 抽象類以及虛函數(shù)的幾個結(jié)論
- 9. 總結(jié)以及拓展
1. 前言
繼承和多態(tài)這兩兄弟常常一起出現(xiàn)
繼承是實現(xiàn)多態(tài)的前提!
本章重點:
本篇文章著重講解多態(tài)的概念以及
定義,多態(tài)的底層原理和析構(gòu)函數(shù)重寫
以及函數(shù)重寫的兩個例外條件
多繼承中的虛函數(shù)表關(guān)系.其中,簡單介紹
的部分有抽象類的概念以及定義和
繼承與多態(tài)中的兩個新增關(guān)鍵字
注:如果你不知道什么是繼承,或繼承
的知識掌握不牢固,請先閱讀下面文章:
C++繼承深度剖析
2. 多態(tài)的概念以及定義
概念:
通俗來說,多態(tài)就是多種狀態(tài)
父子對象完成相同任務(wù)會產(chǎn)生不同的結(jié)果
比如:
學生和普通人都去買門票
學生是半價,而普通人是全價
在繼承中構(gòu)成多態(tài)要有兩個條件:
- 必須通過基類的指針或引用調(diào)用虛函數(shù)
- 被調(diào)用的函數(shù)必須是虛函數(shù)
并且子類的虛函數(shù)要被重寫
現(xiàn)在的你可能有一萬個問號
什么是虛函數(shù)?什么是重寫?
沒關(guān)系,我們一步一步講!
關(guān)鍵字virtual加在成員函數(shù)前
這個成員函數(shù)就是虛函數(shù)!
虛函數(shù)的重寫(也叫覆蓋)
:
派生類中有一個跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的,返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
上面的代碼中,BuyTicket函數(shù)就被重寫了!
概念講完,下一步進行實戰(zhàn)!
3. 多態(tài)的實例調(diào)用情況
構(gòu)成多態(tài)的條件就兩個,一定要熟記!
一定要熟記!一定要熟記!重要的事情說三遍
下面是多態(tài)的實例:
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;p1->BuyTicket();p2->BuyTicket();return 0;
}
我們知道一個事實:
基類的指針或引用可以指向/引用
子類的對象,我們稱為切片
p1和p2是基類指針,它們調(diào)用的
函數(shù)恰好還被重寫了,所以這里符合
多態(tài),p1指針指向的內(nèi)容是Person
所以它調(diào)用Person中的函數(shù),然而p2
指針指向的內(nèi)容是Student,所以它
調(diào)用的是Student中的函數(shù)!
依次打印:"買票-全家","買票-半價"
4. 構(gòu)成多態(tài)的兩個特例
特例一:
子類的虛函數(shù)不寫virtual
依舊構(gòu)成多態(tài)
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:void BuyTicket() { cout << "買票-半價" << endl; }
};
Person* p1 = new Person;
Person* p2 = new Student;
p1->BuyTicket();
p2->BuyTicket();
這樣寫也是構(gòu)成多態(tài)的!
特例二:
基類與派生類虛函數(shù)返回值類型不同
也可以構(gòu)成多態(tài)(返回值必須滿足某種條件)
class A{};
class B : public A {};class Person {
public:virtual A* f() {return new A;}
};
class Student : public Person {
public:virtual B* f() {return new B;}
};
父類的返回值要返回父類
子類的返回值要返回子類
注意事項1:
父類不寫virtual,而子類的同名
函數(shù)寫了virtual,這是不構(gòu)成多態(tài)的!
class Person {
public:void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
不構(gòu)成多態(tài)!
注意事項2:
在繼承體系中,父子類的同名
函數(shù)不構(gòu)成重寫就構(gòu)成隱藏,不可能構(gòu)成重載!
5. 多態(tài)的底層原理分析(一)
如果你單純的認為Base類只有一個
整型變量占用空間的話,那你就上當啦!
事實上在32位機器下,這里的結(jié)果是8
在64位機器下,這里的結(jié)果是16!
這是因為它除了有一個變量外,還有
一個指針,此指針指向一個虛函數(shù)表
我們通過以下的代碼來觀察內(nèi)存:
class A
{
public:virtual void func1(){cout << "父類func1";}
private:int _a;
};
class B : public A
{
public:virtual void func1(){cout << "子類func1";}
private:int _b;
};int main()
{A a;B b;return 0;
}
此指針叫虛表指針:vfptr,也就是
virtual function ptr
這個指針并不是直接指向虛函數(shù)的地址
而是指向一個虛函數(shù)表,可以理解位一個
數(shù)組,此數(shù)組中存放著此對象中所有的虛
函數(shù)的地址,它們的關(guān)系可以用下圖表示:
注:不管有沒有繼承體系或多態(tài)
只要有虛函數(shù)就有虛表!
6. 多態(tài)底層原理分析(二)
現(xiàn)在得出一個結(jié)論:有虛函數(shù)的
類對象中還存放了一個虛表指針!
那么父類和子類的虛表指針和指向
的內(nèi)容有什么不同或相同處嗎?
形成多態(tài)現(xiàn)象的原理又是什么?
我來一一解答這些問題:
通過下面的代碼來觀察內(nèi)存情況
得出父子類虛表的關(guān)聯(lián):
class A
{
public:virtual void func1()cout << "父類func1";virtual void func2()cout << "父類func2";
private:int _a;
};
class B : public A
{
public:virtual void func1()cout << "子類func1";
private:int _b;
};
int main()
{A a;B b;return 0;
}
請看下圖觀察情況:
結(jié)論:
父類和子類的虛表指針是不同的
證明父子類各有一張?zhí)摵瘮?shù)表!
函數(shù)func1在子類中被重寫了,所以
父子類虛表中的func1函數(shù)地址是不同的
函數(shù)func2沒有被子類重寫,所以
父子類虛表中的func2函數(shù)地址是相同的
拓展結(jié)論:同一個類的不同對象共用一個虛表
多態(tài)的原理深度剖析:
當一個函數(shù)A被重寫時,它的父類虛表存放
父類函數(shù)A的地址,子類虛表存放的是子類
函數(shù)A的地址!
當父類的指針或引用指向子類空間時
調(diào)用虛函數(shù)時,會到指向?qū)ο蟮奶摫碇?br /> 中找到對應(yīng)的虛函數(shù)地址,進行調(diào)用!
拓展結(jié)論:
父子類都只有A函數(shù)或無函數(shù)時
-
若父類寫了虛函數(shù)A,而子類
甚至沒有寫函數(shù)A,此時子類對象中
存儲的虛函數(shù)地址與父類相同 -
若父類甚至沒有寫函數(shù)A,而子類
直接寫了虛函數(shù)A,則父類對象中沒有
虛表,而子類對象中有虛表(存放A)
7. 多態(tài)中的兩個關(guān)鍵字
final:
修飾虛函數(shù),表示該虛函數(shù)不能被重寫
override:
檢查子類類虛函數(shù)是否重寫了
基類虛函數(shù)如果沒有重寫編譯報錯
8. 抽象類以及虛函數(shù)的幾個結(jié)論
抽象類概念:
在虛函數(shù)的后面寫上 =0 ,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數(shù),派生類才能實例化出對象。純虛函數(shù)規(guī)范了派生類必須重寫
抽象類的只需了解概念,實際中
使用到的場景很少
關(guān)于虛函數(shù)的幾個小結(jié)論:
- 析構(gòu)函數(shù)最好定義為虛函數(shù)
- 構(gòu)造函數(shù)不能定義為虛函數(shù)
- 靜態(tài)成員函數(shù)不能是虛函數(shù)
- 內(nèi)聯(lián)函數(shù)(inline)不能是虛函數(shù)
為什么說析構(gòu)函數(shù)最好定義為虛函數(shù)?
請看下面的例子:
class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生類Student的析構(gòu)函數(shù)重寫了Person的析構(gòu)函數(shù)
//下面的delete對象調(diào)用析構(gòu)函數(shù),才能構(gòu)成多態(tài)
//才能保證p1和p2指向的對象正確的調(diào)用析構(gòu)函數(shù)。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
若析構(gòu)函數(shù)不是虛函數(shù),delete ptr2時
不符合多態(tài),ptr2是Person類型指針
就只會調(diào)用Person類的析構(gòu),會有問題
若析構(gòu)函數(shù)是虛函數(shù),delete ptr2時
構(gòu)成多態(tài)的條件,指針指向父類的對象
就調(diào)用父類的析構(gòu),指向子類的對象
就調(diào)用子類的析構(gòu),這樣才是正確的!
9. 總結(jié)以及拓展
多態(tài)在校招的筆試面試中考察的
非常之多,很多面試官都喜歡在這
上面考察學生的掌握C++語法的程度
所以同學們請耐心學習!
拓展閱讀:
多繼承場景下的多態(tài)