世間百態,每個人都扮演著不同的角色,都進行著不同的行為。C++更是如此,C++中也會出現有著不同行為的多種形態的出現,那就讓我們一起進入C++的多態世界吧!!!
一. 多態的概念
多態,顧名思義,就是多種形態。多態分為編譯時多態(靜態多態)和運?時多態(動態多態)。編譯時
多態(靜態多態)就是函數重載和函數模板,它們傳不同類型的參數就可以調?不同的函數,通過參數不同達到多種形態,之所以叫編譯時多態,是因為它們實參傳給形參的參數匹配是在編譯時完成的,我們把編譯時?般歸為靜態,運?時歸為動態。運?時多態,就是傳不同的對象來完成不同的?為,從而達到多種形態。
二. 多態的定義和實現
1.多態的構成條件
多態是?個繼承關系下的類對象,去調?同?函數,產?了不同的?為。
注意: 實現多態還有兩個必須重要條件
① 必須是基類的指針或者引?調?虛函數。
② 被調?的函數必須是虛函數,并且完成了虛函數重寫/覆蓋。
原因:要實現多態,第?必須是基類的指針或引?,因為只有基類的指針或引?才能既指向基類
對象?指向派?類對象;第?派?類必須對基類的虛函數完成重寫/覆蓋,重寫或者覆蓋了,基類和派
?類之間才能有不同的函數,多態的不同形態效果才能達到。
2. 虛函數
類成員函數前?加virtual修飾,那么這個成員函數被稱為虛函數。注意?成員函數不能加virtual修
飾。
示例:
class Person
{
public:virtual void show() { cout << "show()" << endl;}
};virtual void f1() //報錯,非成員函數不能用virtual修飾
{}
3. 虛函數的重寫/覆蓋
派?類中有?個跟基類完全相同的虛函數(即派?類虛函數與基類虛函數的返回值類型、函數名、參數類型列表完全相同),則稱派?類的虛函數重寫/覆蓋了基類的虛函數。
注意:在重寫基類虛函數時,派?類的虛函數在不加virtual關鍵字時,雖然也可以構成重寫但是該種寫法不是很規范,不建議這樣使?。
示例:
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;//多態的概念、定義和實現class Person { //Person是父類
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};class Student : public Person { //Student是子類
public://虛函數重寫virtual void BuyTicket() { cout << "買票-打折" << endl; }
};void Func(Person* ptr)
{//這里的函數BuyTicket()的行為和ptr指向的對象有關ptr->BuyTicket();
}//virtual void f1() //報錯,非成員函數不能用virtual修飾
//{}int main()
{Person ps;Student st;Func(&ps);Func(&st);ps.BuyTicket();st.BuyTicket();return 0;
}
多態示例:
class Animal //父類
{
public:virtual void talk() const{}
};class Dog : public Animal //子類
{
public:virtual void talk() const{cout << "汪汪" << endl;}
};class Cat : public Animal //子類
{
public:virtual void talk() const{cout << "喵喵" << endl;}
};void letsHear(const Animal& animal)
{animal.talk();
}int main()
{Cat cat;Dog dog;letsHear(cat);letsHear(dog);return 0;
}
多態應用:
class A
{
public:virtual void func(int val = 1) { cout << "A->" << val <<endl; }virtual void test() { func(); } //test()的A* this,把B*對象傳給this
};class B : public A
{
public:void func(int val = 0) { cout << "B->" << val << endl; }
};int main(int argc, char* argv[])
{B* p = new B;//A* p1 = new A;p->test(); //輸出B->1p->func(); //輸出B->0return 0;
}
4. 虛函數重寫中的協變
派?類在重寫基類虛函數時,與基類虛函數的返回值類型不同。即基類虛函數返回基類對象的指針或者引?,派?類虛函數返回派?類對象的指針或者引?時,稱為協變。協變的實際運用很少。
示例:
class A {};class B : public A {};class Person {
public://基類虛函數返回基類對象的指針或引用virtual A* BuyTicket(){cout << "買票-全價" << endl;return nullptr;}
};class Student : public Person {
public://派生類虛函數返回派生類對象的指針或引用virtual B* BuyTicket(){cout << "買票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}
5. 析構函數的重寫
當基類的析構函數為虛函數時,派?類的析構函數只要定義了,?論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派?類析構函數名字不同看起來不符合重寫的規則,但實際上編譯器對這里的析構函數的名稱做了特殊處理,編譯后析構函數的名稱統?處理成destructor,所以基類的析構函數加了virtual修飾,派?類的析構函數就構成重寫。
下?的代碼我們可以看到,如果~A(),不加virtual,那么delete p2時只調?的A的析構函數,沒有調?
B的析構函數,就會導致內存泄漏問題,因為~B()中有資源需要釋放。
示例:
class A
{
public://~A() //有問題,會導致內存泄漏virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};// 只有子類Student的析構函數重寫了父類Person的析構函數,下?的delete對象調?析構函數時才能
//構成多態,才能保證p1和p2指向的對象正確的調?析構函數,防止內存泄漏。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
6. override和final關鍵字
C++11提供了override關鍵字,可以幫助用戶檢查虛函數是否重寫。如果我們不想讓子類重寫這個虛函數,那么可以?final去修飾基類的虛函數。
示例1:
class Car {
public:virtual void Dirve(){}
};class Benz :public Car {
public://報錯,override修飾的成員函數不能重寫基類成員//virtual void Drive() override { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}
示例2:
class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public://報錯,基類中被final修飾的成員函數不能被子類重寫//virtual void Drive() { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}
7. 重載/重寫/隱藏的對比
注意: 針對多態,重定義也叫隱藏。
三. 純虛函數和抽象類
在虛函數的后?寫上 =0 ,則這個函數就為純虛函數,純虛函數可以定義實現但是沒有必要,只要聲明即可。包含純虛函數的類叫做抽象類,抽象類不能實例化出對象,如果派?類繼承后不重寫純虛函數,那么派?類也是抽象類。純虛函數某種程度上強制了派?類重寫虛函數,因為不重寫派生類不能實例化對象。
示例:
class Car //抽象類
{
public:virtual void Drive() = 0; //純虛函數
};class Benz :public Car
{
public://若父類是抽象類,則未重寫父類純虛函數的子類也是抽象類virtual void Drive(){cout << "Benz-comfort" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-control" << endl;}
};int main()
{//Car car; //報錯,抽象類不能實例化對象Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}
四.多態的原理
1. 虛函數表指針
示例:
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;//12byte,每個有虛函數的類對象上都會在前面存一個_vfptr的虛函數表指針,指向一個虛函數表//虛函數表存儲該類的所有虛函數地址cout << sizeof(b) << endl;return 0;
}
如下圖:
2. 多態的原理
示例:
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }virtual void func1() {}void func2() { }
private:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-打折" << endl; }virtual void func1() {}void func3() {}
private:string _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "買票-優先" << endl; }
private:string _codename;
};void Func(Person* ptr)
{//這里的函數BuyTicket()的行為和ptr指向的對象有關ptr->BuyTicket();
}int main()
{//多態也會發?在多個派生類之間Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}
通過以上代碼和下圖我們可以看到,滿?多態條件后,底層不再是編譯時通過調?對象來確定函數的地址,?是運?時到對象指向的虛表中確定對應的虛函數的地址,這樣就實現了指針或引?指向基類就調?基類的虛函數,指向派?類就調?派?類對應的虛函數。第?張圖,ptr指向的Person對象,調?的是Person的虛函數;第?張圖,ptr指向的Student對象,調?的是Student的虛函數。
3. 動態綁定和靜態綁定
① 對不滿?多態條件的函數調?是在編譯時綁定,也就是編譯時確定所調?函數的地址,叫做靜態綁定。
② 滿?多態條件的函數調?是在運?時綁定,也就是在運?時到指針指向對象的虛函數表中找到所調?函數的地址,也就做動態綁定。
4. 虛函數表
①基類對象的虛函數表中存放基類所有虛函數的地址。同類型的對象共?同?張虛表,不同類型的對
象各自有獨?的虛表,所以基類和派?類有各自獨立的虛表。
② 派?類成員由兩部分構成,繼承下來的基類成員和自己的成員,?般情況下,繼承下來的基類中有虛函數表指針,派生類自己就不會再?成虛函數表指針。但是這?繼承下來的基類部分虛函數表指針和基類對象的虛函數表指針不是同?個,就像基類對象的成員和派?類對象中的基類對象成員一樣是獨立的。
③ 派?類中重寫的基類的虛函數,派?類的虛函數表中對應的虛函數就會被覆蓋成派?類重寫的虛函
數地址。
④ 派?類的虛函數表中包含三個部分:(1)基類的虛函數地址。(2)派?類重寫的虛函數地址完成覆蓋。(3)派?類自己的虛函數地址。
⑤ 虛函數表本質就是?個存儲虛函數表指針的指針數組。
⑥ 虛函數和普通函數?樣,編譯好后是?段指令,都是存在代碼段的,只是虛函數的地址?存到了虛表中。
⑦ vs下虛函數表是存儲在在代碼段(常量區)的。
示例:
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重寫基類的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("棧:%p\n", &i);printf("靜態區:%p\n", &j);printf("堆:%p\n", p1);printf("常量區:%p\n", p2);Base b1;Derive d1;Base* p3 = &b1;Derive* p4 = &d1;printf("Person虛表地址:%p\n", *(int*)p3);printf("Student虛表地址:%p\n", *(int*)p4);printf("Base虛函數地址:%p\n", &Base::func1);printf("Base普通函數地址:%p\n", &Base::func5);return 0;
}
如圖:
How time flies!!! 相信看到了這里的老鐵一定對C++的多態有了初步甚至說是更深的理解,那各位我們就后會有期了!!!