一:基本定義
多態(tài)按字面的意思就是多種形態(tài)。當類之間存在層次結構,并且類之間是通過繼承關聯(lián)時,就會用到多態(tài)。
C++ 多態(tài)意味著調(diào)用成員函數(shù)時,會根據(jù)調(diào)用函數(shù)的對象的類型來執(zhí)行不同的函數(shù)。
下面的實例中,基類 Shape 被派生為兩個類,如下所示:
#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);
// 存儲矩形的地址
shape = &rec;
// 調(diào)用矩形的求面積函數(shù) area
shape->area();
// 存儲三角形的地址
shape = &tri;
// 調(diào)用三角形的求面積函數(shù) area
shape->area();
return 0;
}
當上面的代碼被編譯和執(zhí)行時,它會產(chǎn)生下列結果:
Parent class area
Parent class area
導致錯誤輸出的原因是,調(diào)用函數(shù) area() 被編譯器設置為基類中的版本,這就是所謂的靜態(tài)多態(tài),或靜態(tài)鏈接 - 函數(shù)調(diào)用在程序執(zhí)行前就準備好了。有時候這也被稱為早綁定,因為 area() 函數(shù)在程序編譯期間就已經(jīng)設置好了。
但現(xiàn)在,讓我們對程序稍作修改,在 Shape 類中,area() 的聲明前放置關鍵字 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;
}
};
修改后,當編譯和執(zhí)行前面的實例代碼時,它會產(chǎn)生以下結果:
Rectangle class area
Triangle class area
此時,編譯器看的是指針的內(nèi)容,而不是它的類型。因此,由于 tri 和 rec 類的對象的地址存儲在 *shape 中,所以會調(diào)用各自的 area() 函數(shù)。
正如您所看到的,每個子類都有一個函數(shù) area() 的獨立實現(xiàn)。這就是多態(tài)的一般使用方式。有了多態(tài),您可以有多個不同的類,都帶有同一個名稱但具有不同實現(xiàn)的函數(shù),函數(shù)的參數(shù)甚至可以是相同的。
二:虛函數(shù)
虛函數(shù) 是在基類中使用關鍵字 virtual 聲明的函數(shù)。在派生類中重新定義基類中定義的虛函數(shù)時,會告訴編譯器不要靜態(tài)鏈接到該函數(shù)。
我們想要的是在程序中任意點可以根據(jù)所調(diào)用的對象類型來選擇調(diào)用的函數(shù),這種操作被稱為動態(tài)鏈接,或后期綁定。
三:純虛函數(shù)
您可能想要在基類中定義虛函數(shù),以便在派生類中重新定義該函數(shù)更好地適用于對象,但是您在基類中又不能對虛函數(shù)給出有意義的實現(xiàn),這個時候就會用到純虛函數(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ù)。
四:注意事項
1、純虛函數(shù)聲明如下: virtual void funtion1()=0; 純虛函數(shù)一定沒有定義,純虛函數(shù)用來規(guī)范派生類的行為,即接口。包含純虛函數(shù)的類是抽象類,抽象類不能定義實例,但可以聲明指向?qū)崿F(xiàn)該抽象類的具體類的指針或引用。
2、虛函數(shù)聲明如下:virtual ReturnType FunctionName(Parameter) 虛函數(shù)必須實現(xiàn),如果不實現(xiàn),編譯器將報錯,錯誤提示為:
error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"
3、對于虛函數(shù)來說,父類和子類都有各自的版本。由多態(tài)方式調(diào)用的時候動態(tài)綁定。
4、實現(xiàn)了純虛函數(shù)的子類,該純虛函數(shù)在子類中就編程了虛函數(shù),子類的子類即孫子類可以覆蓋該虛函數(shù),由多態(tài)方式調(diào)用的時候動態(tài)綁定。
5、虛函數(shù)是C++中用于實現(xiàn)多態(tài)(polymorphism)的機制。核心理念就是通過基類訪問派生類定義的函數(shù)。
6、在有動態(tài)分配堆上內(nèi)存的時候,析構函數(shù)必須是虛函數(shù),但沒有必要是純虛的。
7、友元不是成員函數(shù),只有成員函數(shù)才可以是虛擬的,因此友元不能是虛擬函數(shù)。但可以通過讓友元函數(shù)調(diào)用虛擬成員函數(shù)來解決友元的虛擬問題。
8、析構函數(shù)應當是虛函數(shù),將調(diào)用相應對象類型的析構函數(shù),因此,如果指針指向的是子類對象,將調(diào)用子類的析構函數(shù),然后自動調(diào)用基類的析構函數(shù)。
C++多態(tài)意味著調(diào)用成員函數(shù)時,會根據(jù)調(diào)用函數(shù)的對象的類型來執(zhí)行不同的函數(shù);
形成多態(tài)必須具備三個條件:
1、必須存在繼承關系;
2、繼承關系必須有同名虛函數(shù)(其中虛函數(shù)是在基類中使用關鍵字Virtual聲明的函數(shù),在派生類中重新定義基類中定義的虛函數(shù)時,會告訴編譯器不要靜態(tài)鏈接到該函數(shù));
3、存在基類類型的指針或者引用,通過該指針或引用調(diào)用虛函數(shù);
五:動態(tài)聯(lián)編的實現(xiàn)機制 VTABLE
編譯器對每個包含虛函數(shù)的類創(chuàng)建一個虛函數(shù)表VTABLE,表中每一項指向一個虛函數(shù)的地址,即VTABLE表可以看成一個函數(shù)指針的數(shù)組,每個虛函數(shù)的入口地址就是這個數(shù)組的一個元素。
每個含有虛函數(shù)的類都有各自的一張?zhí)摵瘮?shù)表VTABLE。每個派生類的VTABLE繼承了它各個基類的VTABLE,如果基類VTABLE中包含某一項(虛函數(shù)的入口地址),則其派生類的VTABLE中也將包含同樣的一項,但是兩項的值可能不同。如果派生類中重載了該項對應的虛函數(shù),則派生類VTABLE的該項指向重載后的虛函數(shù),如果派生類中沒有對該項對應的虛函數(shù)進行重新定義,則使用基類的這個虛函數(shù)地址。
在創(chuàng)建含有虛函數(shù)的類的對象的時候,編譯器會在每個對象的內(nèi)存布局中增加一個vptr指針項,該指針指向本類的VTABLE。在通過指向基類對象的指針(設為bp)調(diào)用一個虛函數(shù)時,編譯器生成的代碼是先獲取所指對象的vtb1指針,然后調(diào)用vtb1所指向類的VTABLE中的對應項(具體虛函數(shù)的入口地址)。
當基類中沒有定義虛函數(shù)時,其長度=數(shù)據(jù)成員長度;派生類長度=自身數(shù)據(jù)成員長度+基類繼承的數(shù)據(jù)成員長度;
當基類中定義虛函數(shù)后,其長度=數(shù)據(jù)成員長度+虛函數(shù)表的地址長度;派生類長度=自身數(shù)據(jù)成員長度+基類繼承的數(shù)據(jù)成員長度+虛函數(shù)表的地址長度。
包含一個虛函數(shù)和幾個虛函數(shù)的類的長度增量為0。含有虛函數(shù)的類只是增加了一個指針用于存儲虛函數(shù)表的首地址。
派生類與基類同名的虛函數(shù)在VTABLE中有相同的索引號(或序號)。
虛函數(shù)這里說的有些亂,因為 C++ 寫法奇葩略多。其實可以簡單理解。
虛函數(shù)可以不實現(xiàn)(定義)。不實現(xiàn)(定義)的虛函數(shù)是純虛函數(shù)。
在一個類中如果存在未定義的虛函數(shù),那么不能直接使用該類的實例,可以理解因為未定義 virtual 函數(shù),其類是抽象的,無法實例化。將報錯誤:
undefined reference to `vtable for xxx'
這和其它語言的抽象類,抽象方法是類似的——我們必須實現(xiàn)抽象類,否則無法實例化。(virtual 和 abstract還是有些區(qū)別的)
也就是說,如果存在以下代碼:
using namespace std;
class Base {
public:
virtual void tall();
};
class People : Base {
public:
void tall() {
cout << "people" << endl;
};
};
那么,在 main 方法中,我們不能使用 Base base; 這行代碼,此時的 tall 沒有實現(xiàn),函數(shù)表(vtable)的引用是未定義的,故而無法執(zhí)行。但我們可以使用 People people; 然后 people.tall(); 或 (&people)->tall(); 因為People實現(xiàn)或者說重寫、覆蓋了 Base 的純虛方法 tall(),使其在 People 類中有了定義,函數(shù)表掛上去了,于是可以誕生實例了。
int main() {
//??? Base base;//不可用
People people;//可用
people.tall();
(&people)->tall();
return 0;
}
上述的是針對虛函數(shù)而言,普通的函數(shù),即使我們只聲明,不定義,也不會產(chǎn)生上述不可用的問題。
六:其他虛函數(shù)注意事項
父類的虛函數(shù)或純虛函數(shù)在子類中依然是虛函數(shù)。有時我們并不希望父類的某個函數(shù)在子類中被重寫,在 C++11 及以后可以用關鍵字 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();
}
以上程序運行結果:
This is _Base
This is _Base
This is _Base
如果不希望一個類被繼承,也可以使用 final 關鍵字。
格式如下:
class Class_name final
{
...
};
則該類將不能被繼承。