李家宏 孫慶英
摘要:多態(tài)性特征是C++中最為重要的一個特征,熟練使用多態(tài)是學(xué)好C++的關(guān)鍵,而理解多態(tài)的實現(xiàn)機(jī)制及實現(xiàn)過程則是熟練使用多態(tài)的關(guān)鍵。文章在分析多態(tài)性基本屬性的基礎(chǔ)上,結(jié)合具體程序?qū)嵗攸c分析了動態(tài)多態(tài)的實現(xiàn)機(jī)制,并結(jié)合虛函數(shù)和聯(lián)編原理分析了動態(tài)多態(tài)的實現(xiàn)過程。
關(guān)鍵詞:C++;多態(tài)性;虛函數(shù)
中圖分類號:TP312.1? 文獻(xiàn)標(biāo)志碼:A
0 引言
面向?qū)ο蟪绦蛟O(shè)計(Object Oriented Programming)是以對象為程序的基本單元,將數(shù)據(jù)和操作封裝其中,提高了軟件的重用性、靈活性和擴(kuò)展性,C++是面向?qū)ο蟪绦蛟O(shè)計語言的主流之一?,F(xiàn)實世界的諸多事物,包括一些抽象規(guī)則、計劃或事件都可以描述成對象。對象是由數(shù)據(jù)(描述事物的屬性)和作用于數(shù)據(jù)的操作(事物的行為)構(gòu)成的一個獨立整體。
封裝、繼承和多態(tài)是面向?qū)ο笤O(shè)計的3大特點。封裝就是把客觀事物抽象得到的數(shù)據(jù)和行為封裝成一個整體,在C++中,實現(xiàn)數(shù)據(jù)和行為封裝的程序單元就叫類。封裝就是將代碼模塊化,實現(xiàn)了類內(nèi)部對象的隱蔽。繼承是由已經(jīng)存在的類創(chuàng)建新類的機(jī)制,體現(xiàn)在類的層次關(guān)系中,子類擁有父類中的數(shù)據(jù)和方法,子類繼承父類的同時可以修改和擴(kuò)充自己的功能。多態(tài)是指父類的方法被子類重寫、可以各自產(chǎn)生自己的功能行為。封裝和繼承的目的是代碼的重用,多態(tài)就是實現(xiàn)接口重用,即“一個接口,多種方法”。相比封裝和繼承,多態(tài)因其復(fù)雜性、靈活性更難以掌握和理解。
1 多態(tài)的概念
多態(tài)(polymorphism)一詞最早來源于拉丁語poly(意為多)和morphos(意為形態(tài)),意指具有多種形式或形態(tài)。它反映了人們在思索解決問題的辦法時,對相似的問題的一種求解方法[1]。
多態(tài)性一詞最早來源于生物學(xué),是指地球上所有生物,從食物鏈系統(tǒng)、物種水平、群體水平、基因水平等層次上所體現(xiàn)出的形態(tài)和狀態(tài)的多樣性[2]。多態(tài)性是指同樣的消息被不同類型的對象接收時會產(chǎn)生完全不同的行為,即根據(jù)操作環(huán)境的不同采用不同的處理方式,一組具有相同基本語義的方法能在同一接口下為不同的對象服務(wù)[3]。在C++中利用類繼承的層次關(guān)系來實現(xiàn)多態(tài),通常是把具有通用功能的聲明存放在類層次高的地方,而把實現(xiàn)這一個功能的不同方法放在層次較低的類中,C++語言通過子類重定義父類函數(shù)來實現(xiàn)多態(tài)。
2 多態(tài)的分類
多態(tài)通常分為兩種:通用多態(tài)和特定多態(tài),其中,通用多態(tài)又細(xì)分為參數(shù)多態(tài)和包含多態(tài)[4]。參數(shù)多態(tài)在C++中就是利用函數(shù)模板或類模板,給出的不同參數(shù)類型,得到不同的結(jié)果,實現(xiàn)一個具有多種形態(tài)的結(jié)構(gòu)。包含多態(tài)在C++中的基礎(chǔ)就是虛函數(shù),即同樣的操作可用于一個類型及其子類型。特定多態(tài)細(xì)分為重載多態(tài)和強(qiáng)制多態(tài)。重載多態(tài)在C++中就是函數(shù)重載和運(yùn)算符重載,即同一個名(操作符、函數(shù)名)在不同的上下文中有不同的類型。強(qiáng)制多態(tài),這里強(qiáng)制也稱為類型轉(zhuǎn)換,在C++中一般指基本類型轉(zhuǎn)換和自定義類型轉(zhuǎn)換,即在編譯的時候發(fā)生數(shù)據(jù)混合運(yùn)算時,程序通過語義操作,改變操作對象的類型以符合運(yùn)行時函數(shù)和操作符的要求。通用多態(tài)和特定多態(tài)的區(qū)別是:通用多態(tài)對工作的類型不加限制,允許不同類型的值執(zhí)行相同的代碼,從語義上為相關(guān)聯(lián)性的類型,特定多態(tài)對有限的類型有效。不同類型的值可能要執(zhí)行不同的代碼,從語義上為無關(guān)聯(lián)的類型。
3 多態(tài)的實現(xiàn)
3.1 類型兼容與函數(shù)重寫
C++中的繼承遵循了類型兼容性原則,即當(dāng)子類以Public方式繼承父類時,將繼承父類的所有屬性和方法,因此,可以變相的理解成子類是一種特殊的父類,可以使用子類對象初始化父類,也可以使用父類的指針或引用來調(diào)用子類的對象。
在程序設(shè)計過程中,很多時候會出現(xiàn)這樣一種情況,子類繼承父類的A函數(shù),但父類的A函數(shù)不能滿足子類的需求,此時需要在子類中對A函數(shù)進(jìn)行重寫。C++中的函數(shù)重寫是指:函數(shù)名、參數(shù)、返回類型均相同。如果程序中類型兼容性原則遇到了函數(shù)重寫會怎么樣,調(diào)用父類的A函數(shù)還是子類中重寫的A函數(shù),類型兼容與函數(shù)重寫之間的關(guān)系可以用以下程序代碼闡釋:
#include
using namespace std;
class Animal // 父類
{
public:
void Speak()
{
cout << "動物在說話" << endl;
}
};
class Dog :public Animal// 子類
{
public:
void Speak()
{
cout << "小狗在汪汪叫" << endl;
}
};
int main()
{
Dog dog;
dog.Speak();
dog.Animal::Speak();
Animal animal1 = dog;
animal1.Speak();
Animal * animal2 = & dog;
animal2->Speak();
return 0;
}
Animal animal1 = dog;
Animal * animal2 = & dog;
程序的運(yùn)行結(jié)果如圖1所示。
上述程序中定義了Animal和Dog兩個類,其中,Dog類以Public方式繼承了Animal類,并且重寫了Speak()方法。根據(jù)程序運(yùn)行結(jié)果不難看出:main()函數(shù)中定義的Dog類對象dog的調(diào)用方法dog.Speak()是通過子類對象的Speak()函數(shù)來實現(xiàn)小狗在汪汪叫功能。dog.Animal::Speak()是子類對象通過使用操作符作用域調(diào)用父類的Speak()函數(shù)來實現(xiàn):動物在說話。定義的Animal的對象animal1通過調(diào)用拷貝構(gòu)造函數(shù),把dog的數(shù)據(jù)拷貝到animal1中,animal1仍為父類對象,所以animal1.Speak()執(zhí)行的結(jié)果是動物在說話。最終定義了一個指向Animal類的指針animal2,將派生類對象dog的地址賦給父類指針animal2,利用該變量調(diào)用animal2–>speak()方法。得到的結(jié)果是:動物在說話。原因是C++編譯器進(jìn)行了類型轉(zhuǎn)換,允許父類和子類之間進(jìn)行類型轉(zhuǎn)換,即父類指針可以直接指向子類對象。根據(jù)賦值兼容,編譯器認(rèn)為父類指針指向的是父類對象,因此,編譯結(jié)果只可能是調(diào)用父類中定義的同名函數(shù)。在此時,C++認(rèn)為變量animal2中保存的就是Animal對象的地址,即編譯器不知道指針animal2指向的是一個什么對象,編譯器認(rèn)為最安全的方法就是調(diào)用父類對象的函數(shù),因為父類和子類肯定都有相同的Speak()函數(shù)。因此,在main()函數(shù)中執(zhí)行animal2–>Speak()時,調(diào)用的是Animal對象的Speak()函數(shù)。
3.2 動態(tài)聯(lián)編與靜態(tài)聯(lián)編
以上程序出現(xiàn)這種情況的原因涉及C++在具體編譯過程中函數(shù)調(diào)用的問題,這種確定調(diào)用同名函數(shù)的哪個函數(shù)的過程就叫做聯(lián)編(又稱綁定)。在C++中聯(lián)編就是指函數(shù)調(diào)用與執(zhí)行代碼之間關(guān)聯(lián)的過程,即確定某個標(biāo)識符對應(yīng)的存儲地址的過程,在C++程序中,程序的每一個函數(shù)在內(nèi)存中會被分配一段存儲空間,而被分配的存儲空間的起始地址則為函數(shù)的入口地址。
按照程序聯(lián)編所進(jìn)行的階段,聯(lián)編可分為兩種:靜態(tài)聯(lián)編和動態(tài)聯(lián)編。靜態(tài)聯(lián)編就是在程序的編譯與連接階段就已經(jīng)確定函數(shù)調(diào)用和執(zhí)行該調(diào)用的函數(shù)之間的關(guān)聯(lián)。在生成可執(zhí)行文件中,函數(shù)的調(diào)用所關(guān)聯(lián)執(zhí)行的代碼是確定好的,因此,靜態(tài)聯(lián)編也稱為早綁定(Early Binding)。動態(tài)聯(lián)編是在程序的運(yùn)行時根據(jù)具體情況才能確定函數(shù)調(diào)用所關(guān)聯(lián)的執(zhí)行代碼,因此,動態(tài)聯(lián)編也稱為晚綁定(Late Binding)[5]。
當(dāng)類型兼容原則與函數(shù)重寫發(fā)生沖突時,程序員希望根據(jù)程序設(shè)計的子類對象類型來調(diào)用子類對象的函數(shù),而不是編譯器認(rèn)為的調(diào)用父類的對象函數(shù)。也就是說,如果父類指針(引用)指向(引用)父類的對象時,程序就應(yīng)該調(diào)用父類的函數(shù),如果父類指針(引用)指向(引用)子類的對象時,程序就應(yīng)該調(diào)用子類的函數(shù)。這一功能可以通過動態(tài)聯(lián)編實現(xiàn)。與靜態(tài)聯(lián)編相比,動態(tài)聯(lián)編是在程序運(yùn)行階段,根據(jù)成員函數(shù)基于對象的類型不同,編譯的結(jié)果就不同,這就是動態(tài)多態(tài)。動態(tài)多態(tài)的基礎(chǔ)是虛函數(shù)。虛函數(shù)是用來表現(xiàn)父類和子類成員函數(shù)的一種關(guān)系。
3.3 虛函數(shù)
虛函數(shù)的定義方法是用關(guān)鍵字virtual修飾類的成員函數(shù),虛函數(shù)的定義格式:virtual〈返回值類型〉〈函數(shù)名〉(〈形式參數(shù)表〉)<函數(shù)體>。
在類的層次結(jié)構(gòu)中,成員函數(shù)一旦被聲明為虛函數(shù),那么,該類之后所有派生出來的新類中其都是虛函數(shù)。父類的虛函數(shù)在派生類中可以不重新定義,若在子類中沒有重新改寫父類的虛函數(shù),則調(diào)用父類的虛函數(shù)。對兼容性與函數(shù)重寫程序,進(jìn)行適當(dāng)?shù)男薷?,將父類Animal中的Speak()函數(shù)使用關(guān)鍵子Virtual將其定義為虛函數(shù),代碼如下所示。
#include
using namespace std;
class Animal // 父類
{
public:
virtual void Speak() //用virtual 關(guān)鍵子定義Speak()為虛函數(shù)
{
cout << "動物在說話" << endl;
}
};
class Dog :public Animal// 子類Dog以public方式繼承了Animal
{
public:
void Speak()//重寫了Speak()函數(shù)
{
cout << "小狗在汪汪叫" << endl;
}
};
int main()
{
Dog dog;
dog.Speak();
dog.Animal::Speak();
Animal animal1 = dog;
animal1.Speak();
Animal * animal2 = & dog;
animal2->Speak();
return 0;
}
運(yùn)行結(jié)果如圖2所示。
Animal *animal2=&dog,animal2.Speak()時,由于在父類Animal的Speak()函數(shù)前加關(guān)鍵字Virtual,使得Speak()函數(shù)變成虛函數(shù),編譯器在編譯的時候,發(fā)現(xiàn)animal類中有虛函數(shù),此時,編譯器會為每個包含虛函數(shù)的類創(chuàng)建一個虛函數(shù)表,該表是一個一維數(shù)組,在這個數(shù)組中存放每個虛函數(shù)的地址,這樣就實現(xiàn)了動態(tài)聯(lián)編,也就是晚綁定。也就實現(xiàn)了前面說的當(dāng)調(diào)用父類指針(引用)指向(引用)子類對象函數(shù)時,調(diào)用的是子類對象的函數(shù),實現(xiàn)了動態(tài)多態(tài)。
通過分析發(fā)現(xiàn),要想實現(xiàn)動態(tài)多態(tài)要滿足以下3個條件:(1)必須存在繼承關(guān)系,程序中的Dog類以public的方式繼承了Animal類。(2)繼承關(guān)系中必須要有同名的虛函數(shù)。在兩個類中Speak()函數(shù)為同名虛函數(shù),子類重寫父類的虛函數(shù)。(3)存在父類的指針或引用調(diào)用子類該虛函數(shù)。
了解多態(tài)是如何實現(xiàn)的之前,先要了解虛函數(shù)的調(diào)用原理,虛函數(shù)的調(diào)用原理和普通函數(shù)不一樣,編譯器在程序編譯的時候,發(fā)現(xiàn)類中有關(guān)鍵字virtual的虛函數(shù)時,編譯器會自動為每個包含虛函數(shù)的類創(chuàng)建一個虛函數(shù)表用來存放類對象中虛函數(shù)的地址,并同時創(chuàng)建一個虛函數(shù)表指針指向該虛函數(shù)表[6]。每個類使用一個虛函數(shù)表,每個類對象用一個指向虛表地址的虛表指針。父類對象包含一個指針指向父類所有虛函數(shù)的地址,子類對象也包含一個指向獨立地址的指針。如果子類沒有重新定義虛函數(shù),該虛函數(shù)表將保存函數(shù)原始版本的地址,如果子類提供了虛函數(shù)的新定義,該虛函數(shù)表將保存新函數(shù)的地址。示例程序中定義了兩個類A和B,類B繼承自類A,父類A中定義了兩個虛函數(shù),子類B中重寫了其中一個虛函數(shù),代碼如下所示:
class A
{
public:
virtual void fun1()
{
cout << "fun1是類A虛函數(shù)";
}
virtual void fun2()
{
cout << "fun2是虛類A函數(shù)";
}
};
class B :public A
{
public:
virtual void fun1()
{
cout << "fun1是類B的虛函數(shù)";
}
};
分析上述程序,對于父類A中的兩個虛函數(shù)fun1()和fun2(),由于子類B重寫了類A中的fun1()函數(shù),就導(dǎo)致子類B的虛函數(shù)表的第一個指針指向的是類B的fun1()的函數(shù)而不是父類A的fun1()函數(shù),具體如表1所示。
3.4 動態(tài)多態(tài)的實現(xiàn)過程
編譯器進(jìn)行編譯程序時發(fā)現(xiàn)有virtual聲明的函數(shù),就會在這個類中產(chǎn)生一個虛函數(shù)表。即使子類中沒有用virtual定義虛函數(shù),由于父類中的定義,子類通過繼承后仍為虛函數(shù)。程序中Animal類和Dog類都包含一個虛函數(shù)Speak(),因此,編譯器會為這兩個類都建立一個虛函數(shù)表,將虛函數(shù)地址存放到該表中(見圖3)。
編譯器在為每個類創(chuàng)建虛函數(shù)表的同時,還為每個類的對象提供了一個虛函數(shù)表指針(vfptr),虛函數(shù)表指針指向了對象所屬類的虛表。根據(jù)程序運(yùn)行的對象類型去初始化虛函數(shù)表指針。虛函數(shù)表指針在沒有初始化的情況下,程序是無法調(diào)用虛函數(shù)的。虛函數(shù)表的創(chuàng)建和虛函數(shù)表指針的初始化是在構(gòu)造函數(shù)中實現(xiàn)的,在構(gòu)造子類對象時,先調(diào)用父類的構(gòu)造函數(shù),并初始化父類的虛函數(shù)指針,指向父類的虛函數(shù)表,當(dāng)子類對象執(zhí)行構(gòu)造函數(shù)時,子類對象的虛函數(shù)表指針也被初始化,指向子類的虛函數(shù)表。實現(xiàn)了在調(diào)用虛函數(shù)時,就能夠找到正確的函數(shù),如圖4所示。
C++編譯器在編譯時,發(fā)現(xiàn)Animal類的Speak()函數(shù)是虛函數(shù),此時C++就會采用動態(tài)聯(lián)編技術(shù)。程序編譯時并不確定具體調(diào)用的函數(shù),而是在運(yùn)行時,依據(jù)對象的類型來確認(rèn)調(diào)用的是哪一個函數(shù),這種能力就叫做C++的多態(tài)性。在構(gòu)造子類Dog對象dog時,按照構(gòu)造函數(shù)調(diào)用的順序,先調(diào)用父類Animal的構(gòu)造函數(shù)并初始化父類對象虛函數(shù)表指針,該指針指向父類的虛函數(shù)表。執(zhí)行子類Dog構(gòu)造函數(shù)時,子類對象的虛函數(shù)表指針被初始化,指向自身的虛函數(shù)表。Dog類的dog對象構(gòu)造完畢后,其內(nèi)部虛函數(shù)表指針被初始化為指向Dog類的虛表。在調(diào)用時,根據(jù)虛表中的函數(shù)地址找到Dog類的Speak()函數(shù)完成對虛函數(shù)的調(diào)用,從而實現(xiàn)動態(tài)綁定,實現(xiàn)了動態(tài)多態(tài)。
4 結(jié)語
多態(tài)性作為面向?qū)ο蟪绦蛟O(shè)計語言的3大要素之一,因其靈活性、伸縮性和復(fù)雜性而難以掌握。本文著重分析多態(tài)的分類、特征及動態(tài)多態(tài)的實現(xiàn)機(jī)制和原理,但本文對于動態(tài)多態(tài)的分析僅僅局限于單繼? 承的情況,對于多繼承的情況原理基本相同,本文未作過多說明。
參考文獻(xiàn)
[1]李明明,管志偉.淺析C++多態(tài)的作用及實現(xiàn)原理[J].無線互聯(lián)科技,2014(7):116.
[2]吳克力.C++面向?qū)ο蟪绦蛟O(shè)計[M].北京:清華大學(xué)出版社,2021.
[3]謝云博.多態(tài)性實現(xiàn)機(jī)制在C++與JAVA中的比較分析[J].軟件導(dǎo)刊,2014(6):45-46.
[4]姚云霞.淺析C++中類的多態(tài)性[J].隴東學(xué)院學(xué)報,2012(1):9-11.
[5]劉晨.基于靜態(tài)聯(lián)編與動態(tài)聯(lián)編多態(tài)性的研究[J].價值工程,2010(19):248-249.
[6]柯棟梁,李軍利.C++虛函數(shù)實現(xiàn)多態(tài)之案例驅(qū)動教學(xué)方法探討[J].安徽工業(yè)大學(xué)學(xué)報(社會科學(xué)版),2012(4):114-115.
(編輯 何 琳)
Implementation of C++ polymorphism
Li? Jiahong, Sun? Qingying*
(Huaiyin Normal University, Huaian 223300, China)
Abstract:? Polymorphism is the most important feature in C++. Skillful use of polymorphism is the key to learn C++well, while understanding the implementation mechanism and process of polymorphism is the key to use polymorphism skillfully. Based on the analysis of the basic attributes of polymorphism, this paper focuses on the implementation mechanism of dynamic polymorphism with specific program examples, and analyzes the implementation process of dynamic polymorphism with virtual function and binding principle.
Key words: C++; polymorphism; virtual function