一:基本定義
多態(tài)按字面的意思就是多種形態(tài)瘦陈。當(dāng)類之間存在層次結(jié)構(gòu)踪区,并且類之間是通過繼承關(guān)聯(lián)時(shí)铺韧,就會(huì)用到多態(tài)。
C++ 多態(tài)意味著調(diào)用成員函數(shù)時(shí)蟹略,會(huì)根據(jù)調(diào)用函數(shù)的對(duì)象的類型來執(zhí)行不同的函數(shù)登失。
下面的實(shí)例中,基類 Shape 被派生為兩個(gè)類挖炬,如下所示:
#include
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <
return 0;
}
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <
return (width * height / 2);
}
};
// 程序的主函數(shù)
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle? tri(10,5);
// 存儲(chǔ)矩形的地址
shape = &rec;
// 調(diào)用矩形的求面積函數(shù) area
shape->area();
// 存儲(chǔ)三角形的地址
shape = &tri;
// 調(diào)用三角形的求面積函數(shù) area
shape->area();
return 0;
}
當(dāng)上面的代碼被編譯和執(zhí)行時(shí)揽浙,它會(huì)產(chǎn)生下列結(jié)果:
Parent class area
Parent class area
導(dǎo)致錯(cuò)誤輸出的原因是,調(diào)用函數(shù) area() 被編譯器設(shè)置為基類中的版本茅茂,這就是所謂的靜態(tài)多態(tài)捏萍,或靜態(tài)鏈接 - 函數(shù)調(diào)用在程序執(zhí)行前就準(zhǔn)備好了。有時(shí)候這也被稱為早綁定空闲,因?yàn)?area() 函數(shù)在程序編譯期間就已經(jīng)設(shè)置好了令杈。
但現(xiàn)在,讓我們對(duì)程序稍作修改碴倾,在 Shape 類中逗噩,area() 的聲明前放置關(guān)鍵字 virtual,如下所示:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <
return 0;
}
};
修改后跌榔,當(dāng)編譯和執(zhí)行前面的實(shí)例代碼時(shí)异雁,它會(huì)產(chǎn)生以下結(jié)果:
Rectangle class area
Triangle class area
此時(shí),編譯器看的是指針的內(nèi)容僧须,而不是它的類型纲刀。因此,由于 tri 和 rec 類的對(duì)象的地址存儲(chǔ)在 *shape 中担平,所以會(huì)調(diào)用各自的 area() 函數(shù)示绊。
正如您所看到的,每個(gè)子類都有一個(gè)函數(shù) area() 的獨(dú)立實(shí)現(xiàn)暂论。這就是多態(tài)的一般使用方式面褐。有了多態(tài),您可以有多個(gè)不同的類取胎,都帶有同一個(gè)名稱但具有不同實(shí)現(xiàn)的函數(shù)展哭,函數(shù)的參數(shù)甚至可以是相同的。
二:虛函數(shù)
虛函數(shù) 是在基類中使用關(guān)鍵字 virtual 聲明的函數(shù)闻蛀。在派生類中重新定義基類中定義的虛函數(shù)時(shí)匪傍,會(huì)告訴編譯器不要靜態(tài)鏈接到該函數(shù)。
我們想要的是在程序中任意點(diǎn)可以根據(jù)所調(diào)用的對(duì)象類型來選擇調(diào)用的函數(shù)觉痛,這種操作被稱為動(dòng)態(tài)鏈接役衡,或后期綁定。
三:純虛函數(shù)
您可能想要在基類中定義虛函數(shù)秧饮,以便在派生類中重新定義該函數(shù)更好地適用于對(duì)象映挂,但是您在基類中又不能對(duì)虛函數(shù)給出有意義的實(shí)現(xiàn),這個(gè)時(shí)候就會(huì)用到純虛函數(shù)盗尸。
我們可以把基類中的虛函數(shù) area() 改寫如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
= 0 告訴編譯器柑船,函數(shù)沒有主體,上面的虛函數(shù)是純虛函數(shù)泼各。
四:注意事項(xiàng)
1鞍时、純虛函數(shù)聲明如下: virtual void funtion1()=0; 純虛函數(shù)一定沒有定義,純虛函數(shù)用來規(guī)范派生類的行為扣蜻,即接口逆巍。包含純虛函數(shù)的類是抽象類,抽象類不能定義實(shí)例莽使,但可以聲明指向?qū)崿F(xiàn)該抽象類的具體類的指針或引用锐极。
2、虛函數(shù)聲明如下:virtual ReturnType FunctionName(Parameter) 虛函數(shù)必須實(shí)現(xiàn)芳肌,如果不實(shí)現(xiàn)灵再,編譯器將報(bào)錯(cuò),錯(cuò)誤提示為:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3亿笤、對(duì)于虛函數(shù)來說翎迁,父類和子類都有各自的版本。由多態(tài)方式調(diào)用的時(shí)候動(dòng)態(tài)綁定净薛。
4汪榔、實(shí)現(xiàn)了純虛函數(shù)的子類,該純虛函數(shù)在子類中就編程了虛函數(shù)肃拜,子類的子類即孫子類可以覆蓋該虛函數(shù)痴腌,由多態(tài)方式調(diào)用的時(shí)候動(dòng)態(tài)綁定。
5爆班、虛函數(shù)是C++中用于實(shí)現(xiàn)多態(tài)(polymorphism)的機(jī)制衷掷。核心理念就是通過基類訪問派生類定義的函數(shù)。
6柿菩、在有動(dòng)態(tài)分配堆上內(nèi)存的時(shí)候戚嗅,析構(gòu)函數(shù)必須是虛函數(shù),但沒有必要是純虛的枢舶。
7懦胞、友元不是成員函數(shù),只有成員函數(shù)才可以是虛擬的凉泄,因此友元不能是虛擬函數(shù)躏尉。但可以通過讓友元函數(shù)調(diào)用虛擬成員函數(shù)來解決友元的虛擬問題。
8后众、析構(gòu)函數(shù)應(yīng)當(dāng)是虛函數(shù)胀糜,將調(diào)用相應(yīng)對(duì)象類型的析構(gòu)函數(shù)颅拦,因此,如果指針指向的是子類對(duì)象教藻,將調(diào)用子類的析構(gòu)函數(shù)距帅,然后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)。
C++多態(tài)意味著調(diào)用成員函數(shù)時(shí)括堤,會(huì)根據(jù)調(diào)用函數(shù)的對(duì)象的類型來執(zhí)行不同的函數(shù)碌秸;
形成多態(tài)必須具備三個(gè)條件:
1、必須存在繼承關(guān)系悄窃;
2讥电、繼承關(guān)系必須有同名虛函數(shù)(其中虛函數(shù)是在基類中使用關(guān)鍵字Virtual聲明的函數(shù),在派生類中重新定義基類中定義的虛函數(shù)時(shí)轧抗,會(huì)告訴編譯器不要靜態(tài)鏈接到該函數(shù))恩敌;
3、存在基類類型的指針或者引用横媚,通過該指針或引用調(diào)用虛函數(shù)潮剪;
五:動(dòng)態(tài)聯(lián)編的實(shí)現(xiàn)機(jī)制 VTABLE
編譯器對(duì)每個(gè)包含虛函數(shù)的類創(chuàng)建一個(gè)虛函數(shù)表VTABLE,表中每一項(xiàng)指向一個(gè)虛函數(shù)的地址分唾,即VTABLE表可以看成一個(gè)函數(shù)指針的數(shù)組抗碰,每個(gè)虛函數(shù)的入口地址就是這個(gè)數(shù)組的一個(gè)元素。
每個(gè)含有虛函數(shù)的類都有各自的一張?zhí)摵瘮?shù)表VTABLE绽乔。每個(gè)派生類的VTABLE繼承了它各個(gè)基類的VTABLE弧蝇,如果基類VTABLE中包含某一項(xiàng)(虛函數(shù)的入口地址),則其派生類的VTABLE中也將包含同樣的一項(xiàng)折砸,但是兩項(xiàng)的值可能不同看疗。如果派生類中重載了該項(xiàng)對(duì)應(yīng)的虛函數(shù),則派生類VTABLE的該項(xiàng)指向重載后的虛函數(shù)睦授,如果派生類中沒有對(duì)該項(xiàng)對(duì)應(yīng)的虛函數(shù)進(jìn)行重新定義两芳,則使用基類的這個(gè)虛函數(shù)地址。
在創(chuàng)建含有虛函數(shù)的類的對(duì)象的時(shí)候去枷,編譯器會(huì)在每個(gè)對(duì)象的內(nèi)存布局中增加一個(gè)vptr指針項(xiàng)怖辆,該指針指向本類的VTABLE。在通過指向基類對(duì)象的指針(設(shè)為bp)調(diào)用一個(gè)虛函數(shù)時(shí)删顶,編譯器生成的代碼是先獲取所指對(duì)象的vtb1指針竖螃,然后調(diào)用vtb1所指向類的VTABLE中的對(duì)應(yīng)項(xiàng)(具體虛函數(shù)的入口地址)。
當(dāng)基類中沒有定義虛函數(shù)時(shí),其長(zhǎng)度=數(shù)據(jù)成員長(zhǎng)度;派生類長(zhǎng)度=自身數(shù)據(jù)成員長(zhǎng)度+基類繼承的數(shù)據(jù)成員長(zhǎng)度患民;
當(dāng)基類中定義虛函數(shù)后掘剪,其長(zhǎng)度=數(shù)據(jù)成員長(zhǎng)度+虛函數(shù)表的地址長(zhǎng)度腻格;派生類長(zhǎng)度=自身數(shù)據(jù)成員長(zhǎng)度+基類繼承的數(shù)據(jù)成員長(zhǎng)度+虛函數(shù)表的地址長(zhǎng)度画拾。
包含一個(gè)虛函數(shù)和幾個(gè)虛函數(shù)的類的長(zhǎng)度增量為0。含有虛函數(shù)的類只是增加了一個(gè)指針用于存儲(chǔ)虛函數(shù)表的首地址菜职。
派生類與基類同名的虛函數(shù)在VTABLE中有相同的索引號(hào)(或序號(hào))碾阁。
虛函數(shù)這里說的有些亂,因?yàn)?C++ 寫法奇葩略多些楣。其實(shí)可以簡(jiǎn)單理解。
虛函數(shù)可以不實(shí)現(xiàn)(定義)宪睹。不實(shí)現(xiàn)(定義)的虛函數(shù)是純虛函數(shù)愁茁。
在一個(gè)類中如果存在未定義的虛函數(shù),那么不能直接使用該類的實(shí)例亭病,可以理解因?yàn)槲炊x virtual 函數(shù)鹅很,其類是抽象的,無法實(shí)例化罪帖。將報(bào)錯(cuò)誤:
undefined reference to `vtable for xxx'
這和其它語(yǔ)言的抽象類促煮,抽象方法是類似的——我們必須實(shí)現(xiàn)抽象類,否則無法實(shí)例化整袁。(virtual 和 abstract還是有些區(qū)別的)
也就是說菠齿,如果存在以下代碼:
using namespace std;
class Base {
public:
virtual void tall();
};
class People : Base {
public:
void tall() {
cout << "people" << endl;
};
};
那么,在 main 方法中坐昙,我們不能使用 Base base; 這行代碼绳匀,此時(shí)的 tall 沒有實(shí)現(xiàn),函數(shù)表(vtable)的引用是未定義的炸客,故而無法執(zhí)行疾棵。但我們可以使用 People people; 然后 people.tall(); 或 (&people)->tall(); 因?yàn)镻eople實(shí)現(xiàn)或者說重寫、覆蓋了 Base 的純虛方法 tall()痹仙,使其在 People 類中有了定義是尔,函數(shù)表掛上去了,于是可以誕生實(shí)例了开仰。
int main() {
//??? Base base;//不可用
People people;//可用
people.tall();
(&people)->tall();
return 0;
}
上述的是針對(duì)虛函數(shù)而言拟枚,普通的函數(shù),即使我們只聲明众弓,不定義梨州,也不會(huì)產(chǎn)生上述不可用的問題。
六:其他虛函數(shù)注意事項(xiàng)
父類的虛函數(shù)或純虛函數(shù)在子類中依然是虛函數(shù)田轧。有時(shí)我們并不希望父類的某個(gè)函數(shù)在子類中被重寫暴匠,在 C++11 及以后可以用關(guān)鍵字 final 來避免該函數(shù)再次被重寫。
例:
#include
using namespace std;
class Base
{
public:
virtual void func()
{
cout<<"This is Base"<
}
};
class _Base:public Base
{
public:
void func() final//正確傻粘,func在Base中是虛函數(shù)
{
cout<<"This is _Base"<
}
};
class __Base:public _Base
{
/*??? public://不正確每窖,func在_Base中已經(jīng)不再是虛函數(shù)帮掉,不能再被重寫
void func()
{
cout<<"This is __Base"<
}*/
};
int main()
{
_Base a;
__Base b;
Base* ptr=&a;
ptr->func();
ptr=&b;
_Base* ptr2=&b;??? ptr->func();
ptr2->func();
}
以上程序運(yùn)行結(jié)果:
This is _Base
This is _Base
This is _Base
如果不希望一個(gè)類被繼承,也可以使用 final 關(guān)鍵字窒典。
格式如下:
class Class_name final
{
...
};
則該類將不能被繼承蟆炊。