[转载] C++灵魂所在之---多态的前世与今生
參考鏈接: Java是否支持goto
開頭先送大家一句話吧:? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? ?眾所周知,在20世紀(jì)80年代早期,C++在貝爾實(shí)驗(yàn)室誕生了,這是一門面向?qū)ο蟮恼Z言,但它又不是全新的面向?qū)ο蟮恼Z言,它是在傳統(tǒng)的語言(C語言)進(jìn)行面向?qū)ο髷U(kuò)展而來,但是它有些地方與C語言又有很多區(qū)別,又添加了很多C語言原來沒有的內(nèi)容與概念,所以有些地方是需要花時(shí)間去深入了解的。雖然這兩者有密切關(guān)系,但是即使你很熟悉C語言,想要熟悉C++(先不說熟練掌握或者精通C++),還是得花很大一番功夫,光光是C++之中的三大特性就夠研究好久的了。所以說學(xué)習(xí)C++的過程是一個漫長的過程,如果將來從事和C++(或者說其它任何一門優(yōu)秀的語言)有關(guān)的工作,那么對語言的學(xué)習(xí)可能會貫穿一生,語言是一直發(fā)展而來的,所以說要想成為一個優(yōu)秀的程序員,那么養(yǎng)成每天都學(xué)習(xí)一些新知識的習(xí)慣很重要,我從來都覺得一個人的習(xí)慣很重要,養(yǎng)成了每天學(xué)習(xí)的習(xí)慣,那么有一天突然沒有學(xué)習(xí),你會有一種今天有什么重要任務(wù)沒有完成一樣。但凡那些優(yōu)秀的人,從來都有一個好的習(xí)慣,對于這一點(diǎn)我堅(jiān)信不疑。好了,其它的也不多說了,千里之行,始于足下。? ? ? ? ? ?
? ? ? ?對于C++之中多態(tài)我準(zhǔn)備分三個層次講(當(dāng)然這是我我理解上的三個層次,實(shí)際上這里面的內(nèi)容遠(yuǎn)比我說的要多得多,也更深得多,我是由淺入深,根據(jù)所需要的挑著看吧,當(dāng)然這里的深也只是相對的,如果說C++是一片海的話,那我也只能說我只是見識過淺灘上的風(fēng)景,但是真正海底的神秘我還沒有去研究,希望將來有機(jī)會可以研究到),如果只是單純的想了解一下,就沒有必要整篇都看。前幾天寫了在C++的學(xué)習(xí)過程中關(guān)于繼承方面的一些知識,今天就寫一寫關(guān)于剩下的特性:多態(tài)(封裝性需要說的內(nèi)容比較少,所以就沒有寫)?
? ? ? ?如果你只是想了解一下C++之中關(guān)于多態(tài)的知識,那么第一部分有你要的答案。如果你原本就對C++之中多態(tài)有了解,但是對是想了解內(nèi)層的實(shí)現(xiàn),那么最后一部分會滿足你的好奇心!?
? ? ? 在這里我盜用一下王國維先生對于讀書三境界的總結(jié)來作為我每一層次的小標(biāo)題。??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ?按照我的習(xí)慣總是先從定義入手,當(dāng)然這第一層次肯定了解定義是必須的。首先來看一下多態(tài)的定義:多態(tài)性的英文單詞polymorphism來源于希臘詞根poly(意為“很多”)和morph(意為“形態(tài)”),意思是具有多種形式或形態(tài)的情形,在C++語言中多態(tài)有著更廣泛的含義。在C++之中多態(tài)的定義:多態(tài)性是指具有不同功能的函數(shù)可以用同一個函數(shù)名,這樣就可以用一個函數(shù)名調(diào)用不同內(nèi)容的函數(shù)。在面向?qū)ο蠓椒ㄖ幸话闶沁@樣表述多態(tài)性的:向不同的對象發(fā)送同一個消息, 不同的對象在接收時(shí)會產(chǎn)生不同的行為(即方法)。借用網(wǎng)上的一個牛人對于C++之中多態(tài)總結(jié)的較好的三句話:一、“相同函數(shù)名”,二:“依據(jù)上下文”,三:“實(shí)現(xiàn)卻不同”。個人感覺這三句話總結(jié)地很到位,在這里我就盜用一下。? ? ? ? ?既然這是第一層次,所以在這里有必要先說一下關(guān)于C++之中對象的類型,先再看一張圖:?
??
? ? ? ? ? ? ? ? ?
? ? ? ? ?在了解了對象的類型之后有助于你更好地理解之后的內(nèi)容。?
? ? ? ? 好了,說完定義之后,我們肯定要來說一說多態(tài)的分類了,還是以圖的形式給出,這樣比較直觀,也便于大家去理解:??
??
? ? ? ?接下來我們就來逐個分析一下:?
? ? ? ?先來看一下靜態(tài)多態(tài):編譯器在編譯期間完成的,編譯器根據(jù)函數(shù)實(shí)參的類型(可能會進(jìn)行隱式類型轉(zhuǎn)換),可推 斷出要調(diào)用哪個函數(shù),如果有對應(yīng)的函數(shù)就調(diào)用該函數(shù),否則出現(xiàn)編譯錯誤。?
? ? ? ?其實(shí)我們在之前學(xué)C++過程之中,使用函數(shù)重載的時(shí)候就已經(jīng)用到了多態(tài),只不過這是靜態(tài)多態(tài),如果當(dāng)時(shí)不知道,可能你只是沒有注意或者說沒往多態(tài)這方面想而已。以下內(nèi)容就是一個靜態(tài)多態(tài)。??
int My_Add(int left, int right)
{
? ? return (left + right);
}
?
float My_Add(float left, float right)
{
? ? return (left + right);
}
int main()
{
? ? cout << My_Add(10, 20) << endl;
? ? cout << My_Add(12.34f, 24.68f) << endl;
? ? return 0;
}? ? ? ?
看完靜態(tài)多態(tài)之后,我們就來看一看動態(tài)多態(tài),在沒有特殊說明的情況之下,我們所說的多態(tài)一般都說的是動態(tài)多態(tài)。當(dāng)然在說這個之前先知道什么時(shí)動態(tài)綁定:在程序執(zhí)行期間(非編譯期)判斷所引用對象的實(shí)際類型,根據(jù)其實(shí)際類型調(diào)用相應(yīng)的方法。? ? ? ? 我們用一個例子來說明吧!先看例子,下面再來解釋:?
?
?
class Airport? ? ? ? ? ? ? ? ? ? ? ? ? ? ?//機(jī)場類
{
public:? ? ? ? ? ?
? ? void GoToT1()? ? ? ? ??
? ? {?
? ? ? ? cout << "You want to T1 air terminal--->Please Left" << endl;? ? ? ?//T1候機(jī)樓
? ? }? ? ? ? ? ? ??
? ? void GotoT2()? ? ? ? ??
? ? {?
? ? ? ? cout << "You want to T2 air terminal--->Please Right" << endl;? ? ? //T2候機(jī)樓
? ? }
};
?
class CPerson? ? ? ? ? ? ? ? ? ? ? ? ? ? //人類
{?
public:? ? ? ? ? ?
? ? virtual void GoToTerminal(Airport & _terminal) = 0;?
};
?
class Passage_A :public CPerson? ? ? ? ? //乘客A類(這類乘客需要去T1候機(jī)樓登機(jī))
{?
public:? ? ? ? ? ?
? ? virtual void GoToTerminal(Airport & _terminal)
? ? {
? ? ? ? _terminal.GoToT1();
? ? }
};
?
class Passage_B :public CPerson? ? ? ? ? //乘客B類(這類乘客需要去T2候機(jī)樓登機(jī))
{
public:? ? ? ? ? ?
? ? virtual void GoToTerminal(Airport & _terminal)
? ? {
? ? ? ? _terminal.GotoT2();
? ? }
};
?
void FunTest()?
{
? ? Airport terminal;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //創(chuàng)建機(jī)場類對象
? ? for (int iIdx = 1; iIdx <= 10; ++iIdx)? ? ? ? ? ?
? ? {
? ? ? ? CPerson* pPerson;? ? ? ? ? ? ? ? ? ??
? ? ? ? int iPerson = rand() % iIdx;? ? ? ? ? ? //設(shè)置隨機(jī)值,這樣可以使下面生成不同的子類對象? ? ? ?
? ? ? ? if (iPerson & 0x01)? ? ? ? ? ? ? ? ??
? ? ? ? {?
? ? ? ? ? ? pPerson = new Passage_A;?
? ? ? ? }
? ? ? ? else? ? ? ? ? ? ? ? ??
? ? ? ? {?
? ? ? ? ? ? pPerson = new Passage_B;
? ? ? ? }
? ? ? ? pPerson->GoToTerminal(terminal);
? ? ? ? delete pPerson;? ? ? ? ? ? ? ? ??
? ? ? ? pPerson = NULL;? ? ? ? ? ? ? ? ??
? ? ? ? Sleep(1000);? ? ? ? ? ? ? ? ? ? ? ? ? ? //每次休息1秒后繼續(xù)輸出
? ? }
}
?
int main()
{
? ? FunTest();
? ? return 0;
}?
? ? ? ? ?在這里簡單說明一下,上面代碼首先有一個機(jī)場類,里面有兩個方法,表示兩個候機(jī)樓在不同地方,你需要左拐還是右拐。接下來就是一個人類,里面只有一個純虛函數(shù),因此這個人類就是一個抽象類。把這個抽象類作為基類,又派生出兩個子類。在子類之中把父類之中的純虛函數(shù)重新定義了(這也是必須的),(之前我在關(guān)于繼承那一篇文章之中說過菱形繼承之中就是利用虛擬繼承解決數(shù)據(jù)二義性問題),也就是說這里不存在二義性問題,但是這里似乎更加具體,直接在子類中把父類之中的虛函數(shù)(帶有virtual關(guān)鍵字修飾的函數(shù))重寫了(基類之中是一個純虛函數(shù),派生類必須要重寫它才可以實(shí)例化對象)。?
? ? ? ?簡單說一下重寫(或者說是覆蓋,兩者意思一樣):如果兩個成員函數(shù)處在不同的作用域之中(上面的例子是一個在父類,一個在子類),父類之中有virtual關(guān)鍵字修飾(這一點(diǎn)是必須的),而且它們的的參數(shù)相同,返回值類型也相同(這里有必要說明一中特殊情況就是協(xié)變,協(xié)變之中返回值類型可以不同),那么這樣的兩個成員函數(shù)就構(gòu)成了重寫。(派生類中的這個函數(shù)有沒有virtual關(guān)鍵字修飾無所謂,可加可以不加,因?yàn)榧词故桥缮愔械倪@個函數(shù),它本質(zhì)上也還是一個虛函數(shù))。?
? ? ? ?由于派生類之中對父類的純虛函數(shù)進(jìn)行了重寫,因此我們可以說上述代碼實(shí)現(xiàn)了動態(tài)綁定。使用virtual關(guān)鍵字修飾函數(shù)時(shí),指明該函數(shù)為虛函數(shù)(在上面例子中為純虛函數(shù)),派生類需要重新實(shí)現(xiàn),編譯器將實(shí)現(xiàn)動態(tài)綁定。
?
?
? ? ? ?在使用對象的指針的時(shí)候要千萬注意一下,如下面的例子,沒有實(shí)現(xiàn)動態(tài)綁定。?
?
?int main()??
{??
? ? Person? *p;??
? ? Man? *pm;??
? ? Woman *pw;??
? ? p = &man;? ? ?//如果你去調(diào)用其中的Man類中方法(基類之中也有同名方法),那么它會調(diào)用基類之中的方法
? ? p = &woman;? ?//這是因?yàn)閜的類型是一個基類的指針類型,那么在p看來,它指向的就是一個基類對象,
? ? ? ? ? ? ? ? ? ?//所以調(diào)用了基類函數(shù)。
}
? ? ? ? ? ? ??
最后看一下動態(tài)綁定的條件:?
?
? ? ? ? 1、必須是虛函數(shù) 2、通過基類類型的引用或者指針調(diào)用??
? ? ? ?到這里簡單地總結(jié)一下動態(tài)多態(tài):動態(tài)多態(tài)性是在程序運(yùn)行過程中才動態(tài)地確定操作所針對的對象。它又稱運(yùn)行時(shí)的多態(tài)性(動態(tài)多態(tài)性是通過虛函數(shù)(Virtual fiinction)實(shí)現(xiàn)的)。其實(shí)到了這里,對于多態(tài)這一塊內(nèi)容你已經(jīng)有了初步的了解了,如果你只是想初步了解一下,已經(jīng)足夠了。但是可能還會有一些問題,不過也影響不大,畢竟只是初步認(rèn)識一下,至少你明白了多態(tài)的概念以及使用上的一些注意點(diǎn)。如果你想搞清楚更深一層的一些問題,你可以繼續(xù)閱讀。?
??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
? ? ? 在這一層次上我們對虛函數(shù),純虛函數(shù)作進(jìn)一步了解,動態(tài)綁定原理是什么?然后進(jìn)入一個大內(nèi)容---虛表的概念。?
? ? ? 在上一個內(nèi)容上我們討論過了關(guān)于動態(tài)綁定的概念,這時(shí)候可能會有疑問,動態(tài)綁定是如何實(shí)現(xiàn)的呢?那么接下來我們就來說一說里面原理性的內(nèi)容。??
? ? ? ?再解釋之前我們還是先來理一理一些概念,這樣可以更容易理解里面的內(nèi)容,先做好準(zhǔn)備工作。?
首先是關(guān)于重載、重寫(也可以叫做覆蓋)、隱藏(或者說是重定義)這三者的有關(guān)內(nèi)容,如圖所示:?
??
? ? ? ? 接下來我們再來說一說上面提到過的幾個概念,上面沒有展開講,這里仔細(xì)講一講:?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?純虛函數(shù)??
? ? ? ?在成員函數(shù)的形參后面寫上=0,則成員函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對象(雖然抽象類不能實(shí)例化對象,但是可以定義抽像類的指針)。純虛函數(shù)在派生類中重新定義以后,派生類才能實(shí)例化出對象。如下面代碼中:??
?
class Person?
{? ? ?
? ? ?virtual void Display () = 0;? ?// 純虛函數(shù) protected :? ? ?
? ? ?string _name ;? ? ? ? ? ? ? ? ?// 姓名
};
class Student : public Person?
{
? ? //在這類里面必須要對上面的Display ()方法重新定義以后Student才可以實(shí)力化對象
};
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 協(xié)? 變?
?
? ? ? ?在C++中,只要原來的返回類型是指向類的指針或引用,新的返回類型是指向派生類的指針或引用,覆蓋的方法就可以改變返回類型。這樣的類型稱為協(xié)變返回類型(Covariant returns type)。如以下的兩個函數(shù)就構(gòu)成協(xié)變。當(dāng)然協(xié)變也算一種覆蓋。??
?
class Base?
{? ? ?
? ? Base * FunTest()
? ? {
? ? ? ? //do something
? ? }
};
class Derived : public Base
{
? ? <pre name="code" class="cpp">? ? Derived * FunTest()
? ? {
? ? ? ? //do something
? ? }
};?
?
?
? ? ? ?
? ? ? ?有了上面這些鋪墊之后我們就可以開始一個比較重要的內(nèi)容:關(guān)于虛表的相關(guān)知識。先來以下代碼:?
class CTest
{ public:? ? ? ?
? ? CTest()
? ? {?
? ? ? ? iTest = 10;?
? ? }? ? ? ?
? ? /*virtual */~CTest(){};?
private:? ? ? ?
? ? int iTest;?
};
int main()?
{?
? ? cout << sizeof(CTest) << endl;? ? ? ?
? ? return 0;?
}? ? ? ? ? ?
很容易的得到結(jié)果是4,但是如果將里面的注釋內(nèi)容,也就是virtual關(guān)鍵字放開,那么結(jié)果又會是多少呢?知道的人會覺得這是送分題,但是不知道的人卻一臉茫然,答案是8,所以我們猜想,一定是編譯器對有virtual成員函數(shù)的類做了特殊處理。?
?
? ? ? ?先說個大概吧,簡單地說就是:對于有虛函數(shù)的類,編譯器都會維護(hù)一張?zhí)摫?#xff0c;對象的前四個字節(jié)就是指向虛表的指針。這也就可以解釋為什么上面那個例子的原因了。?
? ? ? ?當(dāng)然我們的問題才剛剛開始,請仔細(xì)看下面一張圖,可能會解決你的一些疑惑!?
??
? ? ? ?我們來分析一下上面的內(nèi)容。我們在在監(jiān)視窗口之中對對象test取地址,發(fā)現(xiàn)雖然類中只有一個數(shù)據(jù)成員,但是發(fā)現(xiàn)另一個內(nèi)容,其實(shí)它就是一個虛表指針,觀察它的類型,發(fā)現(xiàn)它里面放著類似于地址的內(nèi)容,但是我們可以在內(nèi)存之中去查看一下它的內(nèi)容。內(nèi)存之中,虛表指針和數(shù)據(jù)成員是連著一起存放的,所以這個虛表指針一定是有什么作用的。我們再打開一個內(nèi)存窗口,觀察一下這個地址的所指向內(nèi)容里面到底是什么。通過上面的圖我們可以發(fā)現(xiàn),其實(shí)這里面放的還是一個地址,突然間又有些疑惑了,我們可以轉(zhuǎn)到反匯編去看一看,我們用virtual修飾的析構(gòu)函數(shù)的入口地址就是剛剛我們看到的地址。在根據(jù)反匯編我們似乎就明白了什么。(細(xì)心的你會發(fā)現(xiàn)其實(shí)每個虛表的最下面總是放的是0x00000000,這也想相當(dāng)于一個結(jié)束標(biāo)志吧!)?
? ? ? ?這時(shí)候,我們可以再來梳理一下,其實(shí)_vfptr存放的內(nèi)容就是存放函數(shù)地址的地址,即_vfptr指向函數(shù)地址所在的內(nèi)存空間,我們可以用圖來表示:??
??
? ? ? ? 到現(xiàn)在為止,你對虛表以及虛表指針這些概念應(yīng)該已經(jīng)不陌生了,來總結(jié)一下就是:?
? ? ? ?test對象中維護(hù)了一個虛表指針,虛表中存放著虛函數(shù)的地址。對test對象取地址可以看到的是虛表的指針以及它的其它成員變量,這個對象又是如何調(diào)用類中虛函數(shù)的呢?其實(shí)調(diào)用的虛函數(shù)是從虛表中查找的。如果基類中有多個虛函數(shù)的話,那么虛表中也會依次按基類中虛函數(shù)定義順序存放虛函數(shù)的地址,并以0x 00 00 00 00 結(jié)尾。再如果子類中有自己定義的新的虛函數(shù),那么會排在虛函數(shù)表的后邊。在調(diào)用虛函數(shù)時(shí)由編譯器自動計(jì)算偏移取得相應(yīng)的虛函數(shù)地址。?
? ? ? ? ? ?說到這里,第二層次的內(nèi)容主要也講完了,不過還有關(guān)于一些關(guān)于基類與派生類之間是如何利用這個虛表指針的,以及如果函數(shù)之中存在覆蓋或者說沒有覆蓋,那么這虛函數(shù)表是否還是一樣的呢?我把放到下一個內(nèi)容,感興趣的話可以接著往下看。? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
? ? ? ?如果你堅(jiān)持看了下來,那么這一部分可能會有些復(fù)雜,在這一部分主要是對虛表作進(jìn)一步剖析,也就是上面遺留下來的內(nèi)容,以及復(fù)雜一點(diǎn)的帶有虛函數(shù)多繼承對象模型剖析。?
? ? ? ?一個一個來說明,首先是上面遺留的內(nèi)容:就是基類與派生類之間是如何利用這個虛表指針的呢?其實(shí)這個內(nèi)容還是需要分為兩部分來講:一、基類與派生類不存在函數(shù)的覆蓋;二、基類與派生類之間存在函數(shù)的覆蓋。?
? ? ? ?第一種情況比較簡單,在這里我主要用文字說明一下,這樣我們可以將重點(diǎn)放在第二種上面。??
? ? ? ?第一種情況(不存在成員函數(shù)覆蓋):先調(diào)用基類構(gòu)造函數(shù),虛表指針先指向基類虛表,然后調(diào)用子類構(gòu)造函數(shù),子類之中也有虛表指針,而且不是同一個,其實(shí)子類在構(gòu)建起來之前的時(shí)候,這個虛表指針指向的是基類的虛表,但是當(dāng)子類構(gòu)建出來的時(shí)候,虛表指針馬上發(fā)生變化,指向了一個新空間,這個新空間里面存放了基類的虛函數(shù)以及派生類自己的虛函數(shù),在最后放入0x00000000,子類自己的虛表也就構(gòu)建完成了。(這里說明一下:1、虛函數(shù)按照其聲明的順序存在于虛表之中。2、在派生類的虛表之中,前面是基類的虛函數(shù),后面是派生類的虛函數(shù))。?
? ? ? ?接著轉(zhuǎn)入第二種(存在成員函數(shù)覆蓋):以下面代碼分析(結(jié)合后面的圖)?
?
class CBase?
{
public:? ? ? ? ? ?
? ? virtual void FunTest0()
? ? {?
? ? ? ? cout << "CBase::FunTest0()" << endl;
? ? }? ? ? ? ? ?
? ? virtual void FunTest1()
? ? {
? ? ? ? cout << "CBase::FunTest1()" << endl;?
? ? }? ? ? ? ? ?
? ? virtual void FunTest2()
? ? {?
? ? ? ? cout << "CBase::FunTest2()" << endl;
? ? }? ? ? ? ??
? ? virtual void FunTest3()
? ? {?
? ? ? ? cout << "CBase::FunTest3()" << endl;?
? ? }?
};
?
class CDerived :public CBase?
{?
public:? ? ? ? ? ?
? ? virtual void FunTest0()
? ? {?
? ? ? ? cout << "CDerived::FunTest0()" << endl;
? ? }? ? ? ? ? ?
? ? virtual void FunTest1()
? ? {?
? ? ? ? cout << "CDerived::FunTest1()" << endl;
? ? }? ? ? ? ??
? ? virtual void FunTest4()
? ? {?
? ? ? ? cout << "CDerived::FunTest4()" << endl;?
? ? }? ? ? ? ? ?
? ? virtual void FunTest5()
? ? {?
? ? ? ? cout << "CDerived::FunTest5()" << endl;?
? ? }?
};
?
typedef void(*_pFunTest)();
void FunTest() {
? ? CBase base;? ? ? ? ? ?
? ? for (int iIdx = 0; iIdx < 4; ++iIdx)? ? ? ? ??
? ? {
? ? ? ? _pFunTest? pFunTest = (_pFunTest)(*((int*)*(int *)&base + iIdx));? ? ? ? ? ? ? ? ??
? ? ? ? pFunTest();
? ? }
? ? cout << endl;? ? ? ? ??
? ? CDerived derived;? ? ? ? ? ?
? ? for (int iIdx = 0; iIdx < 6; ++iIdx)? ? ? ? ?
? ? {?
? ? ? ? _pFunTest? pFunTest = (_pFunTest)(*((int*)*(int *)&derived + iIdx));? ? ? ? ? ? ? ? ? ?
? ? ? ? pFunTest();?
? ? }
}
void TestVirtual()?
{?
? ? CBase base0;? ? ? ? ?
? ? CDerived derived;? ? ? ? ??
? ? CBase& base1 = derived;?
}
?
int main()?
{
? ? FunTest();? ? ? ? ?
? ? TestVirtual();? ? ? ? ??
? ? return 0;?
}
?
?
?
?
? ? ?說到這里大部分內(nèi)容都已經(jīng)結(jié)束了,好了,也是時(shí)候總結(jié)一下了:?
?
派生類重寫基類的虛函數(shù)實(shí)現(xiàn)多態(tài),要求函數(shù)名、參數(shù)列表、返回值完全相同。(協(xié)變除外)基類中定義了虛函數(shù),在派生類中該函數(shù)始終保持虛函數(shù)的特性只有類的成員函數(shù)才能定義為虛函數(shù),靜態(tài)成員函數(shù)不能定義為虛函數(shù)如果在類外定義虛函數(shù),只能在聲明函數(shù)時(shí)加virtual關(guān)鍵字,定義時(shí)不用加構(gòu)造函數(shù)不能定義為虛函數(shù),雖然可以將operator=定義為虛函數(shù),但最好不要這么做,使用時(shí)容 易混淆不要在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù),在構(gòu)造函數(shù)和析構(gòu)函數(shù)中,對象是不完整的,可能會 出現(xiàn)未定義的行為最好將基類的析構(gòu)函數(shù)聲明為虛函數(shù)。(析構(gòu)函數(shù)比較特殊,因?yàn)榕缮惖奈鰳?gòu)函數(shù)跟基類的析構(gòu) 函數(shù)名稱不一樣,但是構(gòu)成覆蓋,這里編譯器做了特殊處理)虛表是所有類對象實(shí)例共用的?
?
?
? ? ? ?還有一個關(guān)于多重繼承之下的虛表指針的情況分析了,這種情況就比較復(fù)雜了,在這里同樣也要分有無虛函數(shù)的覆蓋這兩種情況來分析。在這里我只簡單地說一下有虛函數(shù)重載的情況,有興趣的下來可以自己去試一試。?
?
class CBase0?
{?
public:? ? ? ? ??
? ? CBase0()
? ? {
? ? ? ? m_iTest = 0xA0;?
? ? }? ? ? ? ??
? ? virtual void Print()
? ? {?
? ? ? ? cout << "m_iTest = " << hex << m_iTest << "? CBase2::Print()" << endl;?
? ? }? ? ? ? ??
? ? int m_iTest;?
};
?
class CBase1?
{?
public:? ? ? ? ?
? ? CBase1()
? ? {?
? ? ? ? m_iTest = 0xB0;
? ? }? ? ? ? ??
? ? virtual void Print()
? ? {?
? ? ? ? cout << "m_iTest = " << hex << m_iTest << "? CBase2::Print()" << endl;?
? ? }? ? ? ? ? ?
? ? int m_iTest;?
};
?
class CBase2
{
public:? ? ? ? ?
? ? CBase2()
? ? {?
? ? ? ? m_iTest = 0xC0;
? ? }? ? ? ? ? ?
? ? virtual void Print()
? ? {?
? ? ? ? cout << "m_iTest = " << hex << m_iTest << "? CBase2::Print()" << endl;?
? ? }? ? ? ? ??
? ? int m_iTest;
};
?
class CDerived :public CBase0, public CBase1, public CBase2?
{?
public:? ? ? ? ??
? ? CDerived()
? ? {
? ? ? ? m_iTest = 0xD0;?
? ? }? ? ? ? ? ?
? ? virtual void Print()
? ? {?
? ? ? ? cout << "m_iTest = " << hex << m_iTest << "? CDerived::Print()" << endl;?
? ? }? ? ? ? ? ?
? ? int m_iTest;?
};
?
void FunTest()?
{?
? ? CDerived derived;? ? ? ? ??
? ? cout << sizeof(derived) << endl;? ? ? ? ??
? ? CBase0& base0 = derived;? ? ? ? ??
? ? base0.Print();? ? ? ? ?
? ? CBase1& base1 = derived;? ? ? ? ?
? ? base1.Print();? ? ? ? ??
? ? CBase2& base2 = derived;
? ? base2.Print();? ? ? ? ?
? ? derived.Print();
}
?
int main()
{
? ? FunTest();
? ? return 0;
}? ? ? ? ? ??
大家可以分析一下FunTest()函數(shù)會打印什么,以及Derived的內(nèi)存布局是怎么樣的呢?我給大家一張圖,大家下來自行分析理解一下:?
?
??
? ? ? ? ?其實(shí)如果你有興趣,還可以自己剖析下菱形虛擬繼承!!!不過肯定會有一些復(fù)雜。?
? ? ? ? ?寫到這里,說實(shí)話真的挺累的,終于可以歇一歇了,O(∩_∩)O,在最后推薦大家一本書:深度探索C++對象模型,這本書肯定是有一定難度的,光聽“深入”兩個字就應(yīng)該覺得有些挑戰(zhàn),不過生活中還是應(yīng)該多一些挑戰(zhàn)。?
? ? ? ? 最后再送大家一句話:不忘初心,方得始終!
總結(jié)
以上是生活随笔為你收集整理的[转载] C++灵魂所在之---多态的前世与今生的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何让图片充满excel单元格_如何在E
- 下一篇: [转载] 1.1Java使用JDBC原生