何為多態
多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。
多態的實現
在繼承的體系下
- 基類中必須有虛函數(被virtual關鍵字修飾的成員函數),在派生類中必須要對基類中的虛函數進行重寫
- 對于虛函數的調用:必須使用基類的指針或者引用調用虛函數
體現多態性
在代碼運行時,基類指針指向哪個類的對象,就調用哪個類的虛函數
class Person
{
public:Person(const string& name,const string& gender,const int age):_name(name), _gender(gender), _age(age){}void BuyTicket(){cout << "全價票" << endl;}
protected:string _name;string _gender;int _age;
};class Studnet :public Person
{
public:Studnet(const string& name, const string& gender, const int age,const int _stuId):Person(name,gender,age), _stuId(_stuId){}void BuyTicket(){cout << "半價票" << endl;}
protected:int _stuId;};class Soldier:public Person
{
public:Soldier(const string& name, const string& gender, const int age, const string& rank):Person(name,gender,age), _rank(rank){}void BuyTicket(){cout << "免費" << endl;}
protected:string _rank;
};void TestBuyTicket(Person& p)
{p.BuyTicket();
}int main()
{Person p("Tom", "男", 18);Studnet st("小帥", "女", 19,1000);Soldier so("威武", "男", 23, "班長");TestBuyTicket(p);TestBuyTicket(st);TestBuyTicket(so);system("pause");return 0;
}
并沒有體現出多態性,如果要讓不同的人買到各自的票,我們可以寫三個重載函數來實現,但是這樣的話,代碼的重復性太高,所以這里就要使用多態。
什么是虛函數
虛函數:就是在類的成員函數的前面加virtual關鍵字
virtual void BuyTicket(){cout << "全價票" << endl;}
什么虛函數的重寫?
虛函數的重寫:**派生類中有一個跟基類的完全相同虛函數,我們就稱子類的虛函數重寫了基類的虛函數,完全相同是指:函數名、參數、返回值都相同。**另外虛函數的重寫也叫作虛函數的覆蓋。
也就是說派生類重寫基類中的某個虛函數—>派生類函數必須要與基類中的虛函數原型完全一致
class Person
{
public:virtual void BuyTicket(){cout << "全價票" << endl;}
};
class Studnet :public Person
{
public:virtual void BuyTicket(){cout << "半價票" << endl;}
};
注意事項
- 基類中與子類中的同名方法不是虛函數,但是子類中是虛函數,不是多態,是繼承中的重定義,同名隱藏。基類同名成員函數前必須加virtual關鍵字
- 子類中把虛函數關鍵字去掉,但是此函數在基類中是虛函數,此時是多態。所以子類同名成員函數前virtual關鍵字是否添加都可以
- 基類虛函數必須要與派生類虛函數的原型完全相同(返回值類型,函數名字(參數列表))
class Base
{
public:virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(int ){cout << "Base::TestFunc2()" << endl;}void TestFunc3(){cout << "Base::TestFunc3()" << endl;}virtual void TestFunc4(){cout << "Base::TestFunc4()" << endl;}
};class Derived :public Base
{
public:virtual void TestFunc1(){cout << "Derived::Testfun1()" << endl;}virtual void TestFunc2(){cout << "Derived::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc3()" << endl;}void TestFunc4(){cout << "Derived::TestFunc4()" << endl;}
};void TestVirtualFunc(Base* pb)
{pb->TestFunc1();pb->TestFunc2(10);pb->TestFunc3();pb->TestFunc4();
}
int main()
{Base b;Derived d;TestVirtualFunc(&b);TestVirtualFunc(&d);system("pause");return 0;
}
我們定義一個函數,形參為基類的指針,然后在主函數中分別創建基類對象,子類對象,然后將他們傳進去,第一個傳的是基類對象,所以是基類的指針指向了基類的對象,所以調的全部都是基類的函數。第二個傳的是子類的對象,就是基類指針指向子類對象,但是因為Func2函數與基類中的原型不一致,Func3()函數基類中沒有加virtual關鍵字,所以這兩個函數并沒有實現多態,都調用的是基類的函數,而1和4調用的是子類的函數,實現了多態。
虛函數重寫的例外:協變
基類中虛函數**返回基類(基類1)**的引用(指針),子類
的虛函數返回子類1(只要繼承于基類1)
的引用(指針)基類和子類虛函數的返回值類型不同
class A{};//基類
class B : public A {}; //子類
class Person {//基類1
public:virtual A* f() {return new A;}//返回值的是基類,不是基類1
};
class Student : public Person {//子類1繼承于基類1
public:virtual B* f() {return new B;}//返回值是子類,不是子類1的
};
虛函數重寫的例外:虛函數
函數名字不同,但是可以構成重寫。編譯器對析構函數的名稱做了特殊處理,編譯后
析構函數的名稱統一處理成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;
}
針對于上面的代碼,如果基類和子類都不寫成虛函數。
基類不是虛函數,但是子類是。程序會出內存泄露
正常的結果
接口繼承和實現繼承
普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。
重載,重寫(覆蓋),重定義(隱藏)
重載 | 重寫(覆蓋) | 重定義(隱藏) |
---|---|---|
兩個函數在同一作用域 | 兩個函數分別在基類和派生類的作用域 | 兩個函數分別在基類和派生類的作用域 |
函數名/參數相同 | 函數名/參數/返回值都必須是相同的(協變例外) | 函數名相同 |
兩個函數必須是虛函數 | 兩個基類和派生類的同名函數不構成重寫就是重定義 |
按值傳參與按指針/引用傳參的區別
void TestBuyTicket(Person *p)
{p->~Person();
}
- 在編譯階段,編譯器無法確認基類的指針到底指向哪個類的對象,因為函數在執行期間才會傳參,因此在編譯期間無法確認虛函數的行為。只能在代碼運行時,才可以確定該基類指針指向哪個類的對象。
void TestBuyTicket(Person p)
{p.~Person();//在調用期間,不論傳遞哪個類的對象,該函數調用的都是基類的
}
- 編譯期間,因為該函數按照值的方式傳參,參數已經確認。因此在編譯階段,就會生成基類的臨時對象,因此該函數在編譯期間可以確定虛函數行為,已經確定調用哪個類的函數。
C++11 override 和 final
override
:只能修飾派生類的虛函數
作用:檢測派生類中的某個虛函數是否重寫了哪個虛函數,防止函數名有時候寫錯,沒有構成重寫。
final
:可以修飾類—表示該類不能被繼承,修飾虛函數—虛函數不能被繼承
// 1.final 修飾基類的虛函數不能被派生類重寫
class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒適" << endl;}
};
抽象類
在虛函數的后面寫上 =0
,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
class WC
{
public:void GoToManRoom(){cout << "go to left"<<endl;}void GoToWoManRoom() {cout << "go to right"<<endl;}
};
//作用:規范后續的接口
class Person
{//不能實例化對象,但可以創建該類的指針
public://純虛函數virtual void GoToWc(WC& wc) = 0;string _name;int _age;
};class Man :public Person
{
public:void GoToWc( WC& wc){wc.GoToManRoom();}
};
class WoMan :public Person
{
public:void GoToWc(WC& wc){wc.GoToWoManRoom();}
};
#include<Windows.h>
#include<time.h>
//Monster 也是抽象類,因為該類沒有重寫基類中的純虛函數
class Monster :public Person
{};
void TestWC(int n)
{WC wc;srand(time(nullptr));for (int i = 0; i < n; ++i){Person* pGuard;if (rand() % 2 == 0)pGuard = new Man;elsepGuard = new WoMan;pGuard->GoToWc(wc);delete pGuard;Sleep(1000);}
}
int main()
{//Person* p;TestWC(10);system("pause");return 0;
}
多態的原理
無繼承
class Base
{
public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b;
};
- 一個類中包含有虛函數,會給該類的對象多增加四個字節,該4字節中的內容是在構造函數中填充的。
- 如果類沒有顯示定義構造函數,編譯器會給該類生成一個默認的構造函數,作用:給對象的前4個字節賦值
- 如果類顯示定義了自己的構造函數,編譯器將會對構造函數進行修改,多增加一條語句,給對象的前4個字節賦值
出現繼承
class Base
{
public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b;
};
class Derived :public Base
{};
虛表指針不一樣,派生類和基類用的不是同一張虛表
基類虛表構建過程
將虛函數按照其在類中的聲明次序依次放到虛表中。
class Base
{
public:Base(){}virtual void TestFunc3(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}int _b;
};
class Derived :public Base
{
public:virtual void TestFunc1(){cout << "Derived::TestFunc1()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc2()" << endl;}int _d;
};
派生類虛表的構建過程
- 將基類虛表中內容拷貝一份放到子類的虛表中
- 如果派生類重寫了基類的某個虛函數,用派生類自己的虛函數地址替換虛表中相同偏移量位置的基類虛函數。
- 派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后。
調用原理
class Base
{
public:Base(){}virtual void TestFunc1(){cout << "Base::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Base::TestFunc3()" << endl;}void TestFunc4(){cout << "Base::TestFunc4()" << endl;}int _b;
};
class Derived :public Base
{
public:virtual void TestFunc1(){cout << "Derived::TestFunc1()" << endl;}virtual void TestFunc2(){cout << "Base::TestFunc2()" << endl;}virtual void TestFunc3(){cout << "Derived::TestFunc3()" << endl;}int _d;
};
//虛函數的調用:通過基類的指針或者引用調用虛函數
void TestVirtual(Base *pb)
{pb->TestFunc1();pb->TestFunc2();pb->TestFunc3();pb->TestFunc4();
}
01104BC3 mov eax,dword ptr [pb]
01104BC6 mov edx,dword ptr [eax] //前兩步,從對象前4個字節取虛表的地址
01104BC8 mov esi,esp
01104BCA mov ecx,dword ptr [pb] //傳遞this指針
01104BCD mov eax,dword ptr [edx+4] //從虛表中找對應的虛函數
01104BD0 call eax //調用虛函數
//重新解析d對象的內存空間,
//將d對象的內存空間按照基類對象方式進行解析
//這個過程并沒有創建新的對象,所以還是調用派生類的函數
Base* pb = (Base*)&d;
pb->TestFunc1();
return 0;
打印虛函數表
單繼承
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();//調用虛函數,f=vTable[i] vTable[i]等于一個虛函數入口地址}cout << endl;
}
int main()
{Base b;Derive d;
// 思路:取出b、d對象的頭4比特,就是虛表的指針,虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr
// 1.先取b的地址,強轉成一個int*的指針
// 2.再解引用取值,就取到了b對象頭4比特的值,這個值就是指向虛表的指針
// 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
// 4.虛表指針傳遞給PrintVTable進行打印虛表
// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有
//放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的 - 生成 - 清理解決方案,再編譯就好了。VFPTR* vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);system("pause");return 0;
}
多繼承
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;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVTable(vTableb2);system("pause");return 0;
}
多繼承派生類虛表的構建過程
- 與單繼承中派生類構建過程相同的
- 對于派生類新增加的虛函數按照其在類中的聲明次序增加到第一個虛表的最后。
總結
- 同一個類的不同對象,在底層共享一張虛表
- 派生類除了繼承基類的東西,如果自己沒有加任何的新東西,雖然派生類的虛表內容和基類的內容是一樣的,但是不是同一張虛表,因為虛表指針不一樣。派生類不會和基類共用同一張虛表派生類也有自己的虛表。
- 基類b對象和派生類d對象虛表是不一樣的,這里我們發現Func1完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,所以虛函數的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
- 虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr。
- 虛表存的是虛函數指針,不是虛函數,虛函數和普通函數一樣的,都是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。那么虛表存在哪的呢?vs下是存在代碼段
問題
什么是多態?
概念
同一個事物在不同場景下可以表現出的多種形態,例如迪迦奧特曼的三種形態。
多態的分類
靜態多態(靜態綁定/早綁定) | 動態多態(動態綁定/晚綁定) |
---|---|
在編譯時,可確定具體哪個函數 | 在編譯階段無法確定函數具體調用哪個函數,必須在代碼運行時才能確定,無法確定基類指針或者引用到底指向哪個類的對象 |
函數重載/模板 | 虛函數+繼承 |
多態的實現條件
- 必須在繼承體系中
- 在基類中必須要有虛函數(被
virtual
關鍵字修飾的成員函數),派生類必須對基類的虛函數進行重寫 - 關于虛函數的調用:必須通過基類的指針或者引用調用虛函數
inline函數可以是虛函數嗎?
不能,因為inline函數沒有地址,無法把地址放到虛函數表中。inline
在編譯時期展開,多態發生在運行時。
靜態成員可以是虛函數嗎?
不能,因為靜態成員函數沒有this指針,使用類型::成員函數的調用方式無法訪問虛函數表,所以靜態成員函數無法放進虛函數表。
構造函數可以是虛函數嗎?
不能,因為對象中的虛函數表指針是在構造函數初始化列表階段才初始化的。
構造函數的作用:
- 初始化類中的成員變量
- 將虛表指針放在對象的前4個字節---->編譯器自己完成。
析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
可以,并且最好把基類的析構函數定義成虛函數。析構函數可以為虛函數---->重寫的一種特例,因為派生類重寫基類中的虛析構函數,名字不一樣。
class Base
{
public:Base(int b):_b(b){cout << "Base::Base()" << endl;}virtual ~Base(){cout << "Base::~Base()" << endl;}int _b;
};
class Derived :public Base
{
public:Derived(int b):Base(b){_p = new int[10];}~Derived(){delete[]_p;}
private:int *_p;
};
int main()
{//靜態類型:聲明變量時的類型----在編譯期間起作用//動態類型:實際引用(指向)的類型----在運行時確定調用哪個類的虛函數Base* pb = new Derived(10);delete pb;//看析構函數是不是虛函數,如果不是用靜態類型,//delete:1.調用析構函數釋放對象中的資源// 2.調用operator delete()釋放對象的空間system("pause");return 0;
}
如果派生類中涉及到動態資源的管理(比如:子類從堆上申請空間),建議:基類中的析構函數最好設置為虛函數,否則可能存在內存泄露
對象訪問普通函數快還是虛函數更快?
首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調用的普通函數快,因為構成多態,運行時調用虛函數需要到虛函數表中去查找。
普通函數(指非虛函數)調用 | 虛函數調用(沒有完全實現多態條件) | 虛函數調用(多態實現條件完全滿足) |
---|---|---|
傳參(如果有參數) | 跟普通函數調用一樣 | 從對象的前4個字節中取虛表地址 |
通過指令call調用該函數(call 函數入口地址) | 傳參(包括this指針) | |
從虛表中獲取函數地址 | ||
調用函數 |
多態的缺陷:降低程序運行的速度
虛函數表是在什么階段生成的,存在哪的?
虛函數是在編譯階段就生成的,一般情況下存在代碼段(常量區)的。