多態(tài)的C++實現
1 多態(tài)的原理
什么是多態(tài)?多態(tài)是面向對象的特性之一,只用父類指針指向子類的對象。
1.1 多態(tài)實現的三個條件
- 存在繼承
- 虛函數重寫
- 父類指針指向子類對象
如下代碼,如果并沒有增加virtual
關鍵字,并不會發(fā)生多態(tài)現象。
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
//多態(tài)的重點:需要加virtual關鍵字,否則不會發(fā)生多態(tài)
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
void add()
{
cout<<"Child: c1 + c2 "<<endl;
}
private:
int c1;
int c2;
};
void play(Parent *base)
{
base->add();
}
int main(int argc, const char * argv[]) {
Child c(1,2,3,4);
Parent *pP = &c;
Child *pC = &c;
play(pP);
play(pC);
return 0;
}
1.2 多態(tài)的實現原理
上面的例子中,在基類的函數增加了virtual
關鍵字后,編譯器會自動為子類對應的方法也會增加virtual
關鍵字
1.2.1 虛函數表
- 當類中聲明了虛函數時,編譯器會自動為類生成一張?zhí)摵瘮当怼?/li>
- 虛函數表一個存儲類成員函數指針的數據結構
- 虛函數表是有編譯器自動生成與維護的
1.2.2 vptr指針
如果存在virtual關鍵字,編譯器在運行時的時候(動態(tài)聯(lián)編)會自動為當前對象增加vptr指針,這個vptr指針指向了當前類的虛函數表。
判斷vptr指針是否存在
//如果vptr指針存在,則對象sizeof()之后,大小會發(fā)生變化
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
int main(int argc, const char * argv[]) {
Parent p(1,2);
//可以分別測試下,添加virtual關鍵字和不加的內存空間大小
cout<<sizeof(p)<<endl;
}
1.2.2 多態(tài)實現原理
- 編譯器發(fā)現存在
virtual
關鍵字,則會為類生成一張?zhí)摵瘮当?/li> - 編譯器會在動態(tài)聯(lián)編(運行時)時為對象添加一個vptr指針
- 有父類對象指向子類對象存在,且執(zhí)行了父類方法
- 如果當前對象有vptr指針存在,則會通過vptr指針找到對應的虛函數表,在虛函數表中查找對應的方法地址,執(zhí)行。
2 vptr指針的分步初始化
2.1 父類構造函數中調用父類的方法,會產生多態(tài)嗎?
如果再父類的構造函數中,調用父類的虛函數。那么在子類對象初始化的時候,會不會產生多態(tài)現象呢,還是仍然調用父類的虛函數呢?
答案是:否。不會產生多態(tài)。因為vptr指針是分步初始化的
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
this->add(); //調用父類的虛函數,這個地方不會產生多態(tài)現象。
}
virtual void add()
{
cout<<"Parent: p1 + p2 "<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
virtual void add()
{
cout<<"Child: c1 + c2 "<<endl;
}
private:
int c1;
int c2;
};
void play(Parent *base)
{
base->add();
}
int main(int argc, const char * argv[]) {
//雖然會調用父類的構造函數,但是仍然是調用父類的add方法
Child c(1,2,3,4);
return 0;
}
2.2 vptr指針的分步初始化
那么vptr指針是如何初始化的呢?
- 編譯器編譯時,基類會產生虛函數表,子類也會產生虛函數表
- 當初始化子類對象的時候,先調用父類的構造函數,同時將子類對象的vptr指針指向父類的虛函數表
- 接下來,調用自己的構造函數,同時將vptr指針指向子類的虛函數表
上面的例子中,當調用父類構造函數時,當前vptr指針仍然指向父類的虛函數表,調用的仍然是父類的add方法,不會產生多態(tài)。
3 多態(tài)帶來的問題
可以使用父類指針指向子類的對象,但是指針的類型卻改變了,最指針進行++或者--操作時,可能帶來意向不到的后果。
#include <iostream>
using namespace std;
class Parent {
public:
Parent(int a, int b)
{
this->p1 = a;
this->p2 = b;
}
virtual void print()
{
cout<<"p1 = "<<p1<<"; p2 = "<<p2<<endl;
}
private:
int p1;
int p2;
};
class Child :public Parent {
public:
Child(int a, int b, int c, int d):Parent(c,d)
{
this->c1 = a;
this->c2 = b;
}
virtual void print()
{
cout<<"c1 = "<<c1<<"; c2 = "<<c2<<endl;
}
private:
int c1;
int c2;
};
int main(int argc, const char * argv[]) {
Child array[] = {Child(1,2,3,4),Child(5,6,7,8),Child(9,10,11,12),Child(13,14,15,17),Child(18,19,20,21)};
Child *c = array;
c->print();
c++;
c->print();
Parent *p = array;
p->print(); //仍然打印的是子類的(沒問題)
p++; //執(zhí)行++操作
p->print(); //執(zhí)行之后,崩潰(因為p++和c++的步長不一樣導致錯誤)
return 0;
}
4 純虛函數和虛基類(抽象類)
- 純虛函數是一個在基類中說明的虛函數,在基類中沒有定義,要求任何派生類都定義自己的版本
- 純虛函數為各派生類提供了一個公共界面(接口的封裝和設計、軟件的模塊功能劃分)
- 純虛函數的聲明
virtual void print() = 0;
- 一個具有純虛函數的基類成為抽象類
注意:
- 抽象類不能建立對象,但可以聲明抽象類的指針
- 抽象類不能作為返回來興,也不能作為參數類型
- 抽象類可以聲明抽象類的引用