一、引言
? ? ? ? 眾所周知,C++有三大特性,它們分別是封裝、繼承和多態,在之前的文章中已經詳細介紹過封裝和繼承了,今天我們將一起學習多態相關的知識,如果還想了解封裝、繼承相關的知識,可以跳轉到以下鏈接:
? ? ? ?
????????1、封裝:C++?類和對象(上)!!!-CSDN博客
? ? ? ? 2、繼承:C++?繼承!!!-CSDN博客
二、多態的概念
? ? ? ? 1、概念
? ? ? ? 通俗來講,多態表示多種狀態,即就是說當面對不同類型、不同特點的對象時,處理一個問題時采用不同的方式從而產生不同的效果,這就是多態
? ? ? ? 2、分類
? ? ? ? 事實上,多態細分之下有兩種,它們分別是靜態多態和動態多態,我們常說的多態事實上代指動態多態,也就是我們今天將要主要討論的內容,在詳細了解了多態的相關知識之后我們將再來理解這兩個概念
? ? ? ? 3、從實際的角度認識多態
? ? ? ? 上面我們介紹了多態的概念,這樣我們可以按圖索驥,大概舉幾個日常生活中常見的多態的實際應用:
? ? ? ? ????????(1).打滴滴
? ? ? ? ? ? ? ? 在打滴滴時,新人用戶常常會享受較大的優惠力度,小編記得在我第一次打滴滴時,價格優惠到了4元,那天的路程還挺遠的,如果放在今天可能會在十元往上,這里就用到了多態的相關知識(猜測),當一個新人用戶和一個老用戶同樣的調用"打車"接口時,卻對應了不同的優惠力度,這正好對應了多態的概念
? ? ? ? ? ? ? ? (2).買票系統
? ? ? ? ? ? ? ? 我們日程生活中會進行各種各樣的買票操作,比如各個景點或者是買回家的車票,不難發現,常見的對象會被平臺分為:普通身份、學生、軍人等
? ? ? ? ? ? ? ? 當這些對象同樣調用買票接口時,普通身份會全家買票,學生是半價買票,軍人常見的則是優先買票,很明顯,不同的對象調用同一接口,產生了不同的效果,對應了多態的概念
? ? ? ? 通過以上兩個常見的概念,我們可以感受到多態的相關知識是存在在我們生活中的方方面面的
三、多態的定義及實現
? ? ? ? 1、虛函數
? ? ? ? 虛函數:即就是被virtual關鍵字修飾的函數:
????????
class Person
{
public:virtual void buy_t(){cout << "全價購票" << endl;}
};
? ? ? ? 2、虛函數的重寫
? ? ? ? 虛函數的重寫:派生類中有一個函數跟基類的虛函數三同(即函數名、函數參數、函數返回值都相同)的函數,那么就稱該派生類重寫(覆蓋)了基類的虛函數,例如:
????????
class Person
{
public:virtual void buy_t(){cout << "全價購票" << endl;}
};
class Student : public Person
{
public:void buy_t(){cout << "半價購票" << endl;}
};
? ? ? ? 以上的情況我們就說Student類重寫了Person類中的buy_t函數
? ? ? ? 但是需要注意的是,虛函數重寫存在以下兩個例外:
? ? ? ? ? ? ? ?
????????????????(1).協變(基類與派生類函數返回值不相同)
? ? ? ? ? ? ? ? 派生類重寫基類虛函數是,與基類函數返回值類型不同,當且僅當一個繼承體系的返回值對應的返回了一個繼承體系(并不限制一定是本地的繼承體系)的指針或引用,這時候仍然構成虛函數重寫,稱為協變(了解即可,不推薦使用)例如:
????????????????
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;}};
? ? ? ? ? ? ? ? (2).析構函數的重寫(基類與派生類析構函數名不相同)
????????????????如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字, 都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同, 看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處 理,編譯后析構函數的名稱統一處理成destructor
? ? ? ? ? ? ? ? 所以為什么要這樣特殊處理析構函數,使它可以構成虛函數重寫呢?,我們從下面一個例子來看:
????????????????
class Person {public:virtual ~Person() {cout << "~Person()" << endl;}};class Student : public Person {public:virtual ~Student() { cout << "~Student()" << endl; }};// 只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函
數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數。
int main(){Person* p1 = new Person;Person* p2 = new Student;
delete p1;delete p2;}return 0;
? ? ? ? ? ? ? ? 上面的代碼中,p1、p2都是Person*的變量,隨后調用delete對這兩個動態申請的空間進行釋放,事實上delete對于自定義類型會調用對應類的析構函數,此時就產生了一個問題:兩個空間都會調用Person的析構函數,這是我們不想看到的,我們希望的是對于p1調用Person的析構函數,而對于p2則是調用Student的析構函數
? ? ? ? ? ? ? ? 這時候我們可以認真的觀察一下我們上面的需求,好像就是使用基類的指針來調用同一個函數,同時我們想讓該調用動作對于不同的對象產生不同的效果,是的,這就是我們前面多態討論過的需求,現在只有一個條件還沒有滿足,就是函數名并不相同,所以我們順理成章的想到要讓編譯器對析構函數名進行特殊處理,這樣在將基類的析構函數寫為虛函數時,自然的就解決了上面的問題
? ? ? ? 3、多態的構成條件
? ? ? ? 多態是在繼承關系中,不同的類對象調用同一函數,產生了不同的行為,比如Student繼承了Person,這時候Person對象全價買票,Student對象半價買票,所以首先的,多態是存在在繼承關系中的
? ? ? ? 在繼承關系中要構成多態還有兩個條件:
? ? ? ? ? ? ? ? (1).必須通過基類的指針或者飲用調用函數
? ? ? ? ? ? ? ? (2).被調用的函數必須是虛函數,同時派生類對基類的虛函數進行重寫
? ? ? ? 下面是構成多態的一個完整例子:
????????
#include <iostream>
using namespace std;
//多態
class Person
{
public:virtual void buy_t(){cout << "全價購票" << endl;}
};
class Student : public Person
{
public:void buy_t(){cout << "半價購票" << endl;}
};
void func(Person& rp)
{rp.buy_t();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
}
? ? ? ? 這一段代碼的運行結果如下:
????????
四、C++11中提供的兩個相關的關鍵字:override和final
? ? ? ? 經過上面的講解,我們發現,C++中構成重寫從而構成多態的過程時非常嚴格的,而在平常的代碼工作中我們很容易會犯一些錯誤,比如:大小寫的問題、字母順序的問題,這些問題產生時是很難發現的,對于這些問題,只是沒有構成重寫,但并沒有編譯、鏈接的錯誤,不會報錯,非常頭疼,所以在C++11中我們提供了override和final兩個關鍵字,它們兩個可以幫助我們檢查這一類問題
? ? ? ? 1、final:該關鍵字有兩個作用
? ? ? ? ? ? ? ? (1).修飾虛函數,被修飾的函數不能被重寫:
????????????????
class Person
{
public:virtual void buy_t ()final//final修飾了該函數{cout << "全價購票" << endl;}
};
class Student : public Person
{
public:void buy_t()//這個位置會報錯:無法重寫“final”函數 "Person::buy_t"{cout << "半價購票" << endl;}
};
????????????????(2).修飾一個類,被修飾的類不能被繼承??
????????????????
#include <iostream>
using namespace std;
//多態
class Person final//使用final修飾這個類
{
public:virtual void buy_t(){cout << "全價購票" << endl;}
};
class Student : public Person//這個位置會報錯:不能將"final"類類型用作基類
{
public:void buy_t(){cout << "半價購票" << endl;}
};
? ? ? ? 2、override:檢查派生類函數是否重寫了基類某個虛函數,如果沒有就報錯
????????
class Person
{
public:virtual void buy_t(){cout << "全價購票" << endl;}
};
class Student : public Person
{
public:void buy_tx() override//override修飾該函數//該位置報錯:使用override修飾的函數不能重寫基類成員{cout << "半價購票" << endl;}
};
五、對比重載、重寫(覆蓋)、重定義(隱藏)
六、抽象類
? ? ? ? 1、概念
? ? ? ? 在虛函數的函數頭之后加上=0,此時該函數被稱為純虛函數,包含純虛函數的類叫做抽象類(也叫做接口類),抽象類不能實例化出對象。派生類繼承之后也不能實例化出對象,只有重寫了純虛函數,派生類才能實例化出對象,純虛函數規范了派生類必須重寫,它更能體現出接口繼承
? ? ? ? 下面的代碼體現出了這種接口繼承的思想:
????????
#include <iostream>
using namespace std;
//多態
class Person
{
public:virtual void buy_t() = 0;};
class Student : public Person
{
public:void buy_t(){cout << "半價購票" << endl;}
};
class Teacher :public Person
{
public:void buy_t(){cout << "十倍價錢購票" << endl;}};
void func(Person& rp)
{rp.buy_t();
}
int main()
{Teacher t;Student s;func(t);func(s);return 0;
}
? ? ? ? 下面是以上代碼的執行結果:
????????? ? ? ?
? ? ? ? 2、接口繼承和實現繼承
?????????普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實 現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成 多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數
七、多態的原理
? ? ? ? 1、虛函數表
? ? ? ? ? ? ? ? (1).引入
// 這里常考一道筆試題:sizeof(Base)是多少?
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};
? ? ? ? ? ? ? ? 我們先通過打印的方式看一下這個問題的結果是多少?
????????????????
? ? ? ? ? ? ? ? (2).解決問題
? ? ? ? ? ? ? ? 可以看到,結果輸出了8(這里要強調一下,小編實在x86的環境下輸出的,環境或者平臺改變可能會影響結果),這是為什么呢?或許含有虛函數的類對象進行了一些特殊處理?接下來我們通過調試的方法來看一下該類對象模型是怎樣的:
????????????????
? ? ? ? ? ? ? ? 經過上面的調試窗口我們知道,原來在Base類中除了_b成員,還多一個__vfptr放在對象的前面(注意有些 平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代 表virtual,f代表function)。一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數 的地址要被放到虛函數表中,虛函數表也簡稱虛表,那么派生類中這個表放了些什么呢?我們接著往下分析
? ? ? ? ? ? ? ? 為了符合多態的情景,我們先對上面的代碼做出以下改造:
????????????????
// 針對上面的代碼我們做出以下改造
// 1.我們增加一個派生類Derive去繼承Base// 2.Derive中重寫Func1// 3.Base再增加一個虛函數Func2和一個普通函數Func3class 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;}
? ? ? ? ? ? ? ? 接下來我們一起觀察這個加強版繼承體系的類對象模型,從而說明派生類中的虛表有什么不同?
????????????????
? ? ? ? ? ? ? ? 可以觀察到:繼承之后的d對象模型中分為兩個部分,分別是Base部分和自己的成員,而在Base部分中也有一個_vfptr指針,這意味著d不會生成自己的虛表指針,而是以繼承的形式沿用了Base類的指針,而兩個指針指向的位置是不同的,這就是說兩個類的虛表是不同的,事實上的確是這樣的,派生類會首先繼承基類的虛表,然后對于重寫過的函數將新的函數指針覆蓋原本的函數指針,形成了屬于自己的虛表
? ? ? ? 2、多態的實現
? ? ? ? 經過上面對于虛表指針和虛表的認識,我們大概也可以想到多態究竟是如何實現的
? ? ? ? 事實上,多態的實現原理就是虛表指針存在在父子類中基類的部分,所以必須使用基類的指針或者引用調用(不能直接使用對象調用是因為對象的切片賦值會丟失信息,而指針和引用的切片賦值不會),同時通過虛表指針我們就可以找到虛表,父子類的虛表不同,找到的函數也就不同,這時候就實現了多態調用函數
? ? ? ? 3、動態綁定與靜態綁定
? ? ? ? ????????(1). 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態, 比如:函數重載
? ? ? ? ? ? ? ? (2).?動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體 行為,調用具體的函數,也稱為動態多態
八、結語
? ? ? ? 這就是本期有關多態的全部內容了,感謝大家的閱讀,歡迎各位于晏、亦菲和我一起交流、學習、進步!!!? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 、? ? ? ? ? ? ? ??
????????????????
? ? ? ? ? ? ??
????????? ? ? ? ? ? ? ? ? ? ??
????????????????? ? ? ??
????????