使用dw做門戶網(wǎng)站營銷軟文寫作
C++多態(tài)概念詳解
- 一,多態(tài)概念
- 二,多態(tài)的定義
- 2.1 多態(tài)構(gòu)成的條件
- 2.2 什么是虛函數(shù)
- 2.3 虛函數(shù)的重寫
- 2.3.1 虛函數(shù)重寫的特例
- 2.3.2 override和final
- 2.4 重載和重寫(覆蓋)和重定義(隱藏)的區(qū)別
- 三,抽象類
- 3.1 概念
- 3.2 接口繼承和實(shí)現(xiàn)繼承
- 四,多態(tài)的原理
- 4.1 虛函數(shù)表
- 4.2 多態(tài)調(diào)用的底層原理
- 4.3 靜態(tài)綁定和動態(tài)綁定
- 五,單繼承和多繼承的虛函數(shù)表
- 5.1 單繼承的虛函數(shù)表
- 5.2 多繼承的虛函數(shù)表
- 六,繼承和多態(tài)的常見問題
一,多態(tài)概念
上節(jié)我們看了繼承,現(xiàn)在我們來看多態(tài)。
那么什么是多態(tài)呢?通俗來說,就是多種形態(tài),具體點(diǎn)就是去完成某個(gè)行為,當(dāng)不同的對象去完成時(shí)會產(chǎn)生出不同的狀態(tài)。
舉個(gè)例子,對于買票這件事,一個(gè)成人去買的話是全票,但如果是學(xué)生則半價(jià),在這件事中成人和學(xué)生都可以買票,但是不同的人買,票價(jià)卻不同,這就是一種多態(tài)行為。
二,多態(tài)的定義
2.1 多態(tài)構(gòu)成的條件
多態(tài)是在不同繼承關(guān)系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。
在繼承中構(gòu)成多態(tài)要滿足兩個(gè)條件:
在子類中對父類的虛函數(shù)進(jìn)行重寫,且必須調(diào)用。
通過父類的指針或者引用調(diào)用虛函數(shù)
那什么是虛函數(shù)及什么是重寫,我們下面就來講解
2.2 什么是虛函數(shù)
其實(shí)虛函數(shù)就是加上virtual的函數(shù), 比如下面的代碼:
class Person {
public:virtual void BuyTicket(){cout << "買票-全價(jià)" << endl;}
};
2.3 虛函數(shù)的重寫
虛函數(shù)要完成重寫,那么重寫就是子類中有一個(gè)和父類一樣的虛函數(shù),這個(gè)虛函數(shù)要求:
函數(shù)名,返回值類型,參數(shù)列表相同
class Person {
public:virtual void BuyTicket() {cout << "買票-全價(jià)" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() {cout << "買票-半價(jià)" << endl; }
};
2.3.1 虛函數(shù)重寫的特例
虛函數(shù)的重寫有兩個(gè)特例:
- 協(xié)變----->重寫的虛函數(shù)的類型可以不一樣,但是要是父子類關(guān)系的指針或者引用
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;}
};
B這個(gè)類是A類的子類,Student類是Person的子類,且都有虛函數(shù)f(),但是這兩個(gè)虛函數(shù)的類型分別是A,B父子類的指針,這就是
協(xié)變
。
- 析構(gòu)函數(shù)的重寫 ----->父子類的析構(gòu)函數(shù)會被統(tǒng)一成destuctor,如果不加virtual構(gòu)成重寫,則會構(gòu)成隱藏,不會調(diào)用到父類的析構(gòu)函數(shù),進(jìn)而造成內(nèi)存泄漏(子類的資源沒有釋放完)
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;
}
- 虛函數(shù)重寫時(shí),父類加了virtual,而子類不加virtual也構(gòu)成重寫(建議加上)
class Person {
public:virtual void BuyTicket() {cout << "買票-全價(jià)" << endl; }
};
class Student : public Person {
public:void BuyTicket() {cout << "買票-半價(jià)" << endl; }
};
2.3.2 override和final
C++中對于重寫的要求比較嚴(yán)格,所以有了這兩個(gè)關(guān)鍵字來檢測是否重寫
現(xiàn)在有這樣一個(gè)問題:如何實(shí)現(xiàn)一個(gè)類,讓其不能被繼承
有兩種辦法:
- 讓父類的構(gòu)造函數(shù)私有,以為子類的構(gòu)造要用到父類的構(gòu)造,但是這樣會讓子類不能實(shí)例化出對象
- 用
final
修飾為最終類
final也可以修飾虛函數(shù),修飾后不能被重寫!
override
加在派生類后面檢查是否完成重寫
2.4 重載和重寫(覆蓋)和重定義(隱藏)的區(qū)別
重載我們在前面學(xué)過,重寫在原理層面也叫覆蓋,上一節(jié)講的隱藏也叫重定義。
看下面的圖我們可以看到三者的區(qū)別:
其實(shí)更深層次來看重寫就是一種特殊的重定義!
三,抽象類
3.1 概念
我們先來看什么是純虛函數(shù),就是在虛函數(shù)后面加上 = 0 ,
virtual void fun () = 0
包含純虛函數(shù)的類叫抽象類(接口類),并且抽象類不能實(shí)例化對象。
抽象類就像某類事物抽象出來的一個(gè)特征,不是一個(gè)具體的東西。例如車是一個(gè)抽象類,但是像寶馬,奧迪,奔馳是車這個(gè)抽象類繼承的具體的可實(shí)例化的類。
抽象類的派生類必須重寫虛函數(shù),否則不能實(shí)例化,因?yàn)椴恢貙懽宇惾匀粫r(shí)抽象類,(間接強(qiáng)制子類重寫虛函數(shù))
3.2 接口繼承和實(shí)現(xiàn)繼承
普通函數(shù)的繼承是一種實(shí)現(xiàn)繼承,派生類繼承基類函數(shù),繼承了實(shí)現(xiàn)為了復(fù)用
虛函數(shù)的繼承是一種接口繼承,繼承了父類的接口為了重寫實(shí)現(xiàn),達(dá)成多態(tài)
。
四,多態(tài)的原理
普通函數(shù)和虛函數(shù)都是存在代碼段的,談到多態(tài)的原理我們就不得不說下類對象的存儲設(shè)計(jì)
如下圖:
一個(gè)類中存放著一個(gè)指向類成員函數(shù)表的指針,而這個(gè)表中存放的是函數(shù)的地址,多態(tài)的原理就和這種存儲結(jié)構(gòu)息息相關(guān)。
4.1 虛函數(shù)表
先來試想一下如何計(jì)算一個(gè)有虛函數(shù)的類的大小:
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main() {Base b;cout << sizeof(b) << endl;return 0;
}
運(yùn)行后我們可以發(fā)現(xiàn)
這是為什么呢?
這是因?yàn)锽ase這個(gè)類中除了_b這個(gè)成員外,還有一個(gè)指針_vfptr,這個(gè)指針是虛函數(shù)表指針(虛表指針),指向的是虛函數(shù)指針數(shù)組。
那么這個(gè)指針指向的表是干嘛的呢,我們繼續(xù)來分析,我們讓派生類Derive去繼承Base類,并且增加虛函數(shù)。
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
經(jīng)過調(diào)試我們可以看到
在Base和Derive類中都有_vfptr指針,指向了一張表,里面貌似存放了虛函數(shù)。而且Derive的這個(gè)表里第一個(gè)存放的是重寫的虛函數(shù),第二個(gè)存放的是Base的第二個(gè)虛函數(shù)。
其實(shí)這個(gè)表是虛函數(shù)表(virtual function table),虛表中存儲的是虛函數(shù)的地址(指針)。
派生類的虛函數(shù)表繼承自父類的虛函數(shù)表,但是會用其自己的虛函數(shù)覆蓋虛表中第一個(gè)位置(所以虛函數(shù)的重寫也叫覆蓋)
重寫時(shí)語法層面的,覆蓋是原理層面的
虛表以空結(jié)尾,并且虛函數(shù)存放的順序和聲明的順序一致
派生類有兩部分,一部分是父類的,一部分是自己的,派生類沒有自己單獨(dú)的虛表,而是繼承的父類的,拷貝父類的虛函數(shù)表,并覆蓋自己重寫的虛函數(shù)
知道了虛表的存在后,我們繼續(xù)探索。
如果派生類有一個(gè)自己的虛函數(shù)呢 ? 會在虛表里怎么存放
虛函數(shù)表是存放在常量區(qū)的,在編譯時(shí)生成好的,虛表指針的初始化是在構(gòu)造函數(shù)初始化列表最前面(所有對象初始化之前)。
同類型的對象共享一個(gè)虛函數(shù)表
4.2 多態(tài)調(diào)用的底層原理
如何做到指向父類調(diào)用父類虛函數(shù),指向子類調(diào)子類呢?
class Person {
public:virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}
運(yùn)行后可以看到:
由上面的圖可知,指向父類時(shí),會在父類的虛函數(shù)表中查找對應(yīng)的虛函數(shù)。 指向子類時(shí),會在切割后的父類(子類中完成對父類虛函數(shù)重寫)的虛函數(shù)表中查找已經(jīng)被覆蓋的對應(yīng)的子類的虛函數(shù)
總結(jié)一下就是,多態(tài)調(diào)用就是在運(yùn)行時(shí)去虛函數(shù)表中找虛函數(shù)的地址來進(jìn)行調(diào)用,所以可以達(dá)到指向父類調(diào)父類,指向子類調(diào)子類虛函數(shù)。
如果去掉 virtual ,則是普通調(diào)用,在編譯時(shí)通過調(diào)用者的類型確定函數(shù)的地址
4.3 靜態(tài)綁定和動態(tài)綁定
簡單來說,靜態(tài)就是編譯時(shí),動態(tài)就是運(yùn)行時(shí),
靜態(tài)綁定是在編譯時(shí)確定程序的行為,也叫靜態(tài)多態(tài)(函數(shù)重載),
動態(tài)綁定是在程序運(yùn)行期間確定程序行為
五,單繼承和多繼承的虛函數(shù)表
在單繼承和多繼承關(guān)系中,我們關(guān)注的是派生類對象的虛表模型,因?yàn)榛惖奶摫砟P颓懊嫖覀円呀?jīng)看過了,沒什么需要特別研究的。
5.1 單繼承的虛函數(shù)表
看下面的代碼:
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
單繼承就是將基類的虛函數(shù)表拷貝下來,將自己重寫的虛函數(shù)覆蓋。
5.2 多繼承的虛函數(shù)表
假設(shè)一個(gè)派生類繼承了兩個(gè)基類,計(jì)算這個(gè)派生類的大小
看下面的代碼:
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main() {Derive d;return 0;
}
派生類繼承了兩個(gè)基類的虛表,所以說有兩張?zhí)摫?#xff0c;并且同時(shí)覆蓋了重寫的虛函數(shù)地址,如果派生類有自己的虛函數(shù),那么這個(gè)虛函數(shù)的地址放在繼承的第一張?zhí)摫碇小?/code>
六,繼承和多態(tài)的常見問題
- 內(nèi)聯(lián)函數(shù)也可以是虛函數(shù),當(dāng)內(nèi)聯(lián)函數(shù)是普通調(diào)用時(shí),其內(nèi)聯(lián)屬性還在,當(dāng)多態(tài)調(diào)用時(shí),會失去其內(nèi)聯(lián)屬性。
- 靜態(tài)成員函數(shù)不能是虛函數(shù),因?yàn)闆]有this指針,無法訪問虛函數(shù)表。
- 構(gòu)造函數(shù)不能是虛函數(shù),因?yàn)樘摫碇羔樖窃跇?gòu)造函數(shù)初始化列表之前初始化的