C++多態(tài)的實(shí)現(xiàn)及原理詳細(xì)解析
1. 用virtual關(guān)鍵字申明的函數(shù)叫做虛函數(shù),虛函數(shù)肯定是類的成員函數(shù)。
2. 存在虛函數(shù)的類都有一個(gè)一維的虛函數(shù)表叫做虛表。類的對象有一個(gè)指向虛表開始的虛指針。虛表是和類對應(yīng)的,虛表指針是和對象對應(yīng)的。
3. 多態(tài)性是一個(gè)接口多種實(shí)現(xiàn),是面向?qū)ο蟮暮诵?。分為類的多態(tài)性和函數(shù)的多態(tài)性。
4. 多態(tài)用虛函數(shù)來實(shí)現(xiàn),結(jié)合動(dòng)態(tài)綁定。
5. 純虛函數(shù)是虛函數(shù)再加上= 0。
6. 抽象類是指包括至少一個(gè)純虛函數(shù)的類。
純虛函數(shù):virtual void breathe()=0;即抽象類!必須在子類實(shí)現(xiàn)這個(gè)函數(shù)!即先有名稱,沒內(nèi)容,在派生類實(shí)現(xiàn)內(nèi)容!
我們先看一個(gè)例子:
#include <iostream.h>
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
void main()
{
fish fh;
animal *pAn=&fh; // 隱式類型轉(zhuǎn)換
pAn->breathe();
}
注意,在例1-1的程序中沒有定義虛函數(shù)??紤]一下例1-1的程序執(zhí)行的結(jié)果是什么?
答案是輸出:animal breathe
我們在main()函數(shù)中首先定義了一個(gè)fish類的對象fh,接著定義了一個(gè)指向animal類的指針變量pAn,將fh的地址賦給了指針變量pAn,然后利用該變量調(diào)用pAn->breathe()。許多學(xué)員往往將這種情況和C++的多態(tài)性搞混淆,認(rèn)為fh實(shí)際上是fish類的對象,應(yīng)該是調(diào)用fish類的breathe(),輸出“fish bubble”,然后結(jié)果卻不是這樣。下面我們從兩個(gè)方面來講述原因。
1、 編譯的角度
C++編譯器在編譯的時(shí)候,要確定每個(gè)對象調(diào)用的函數(shù)(要求此函數(shù)是非虛函數(shù))的地址,這稱為早期綁定(early binding),當(dāng)我們將fish類的對象fh的地址賦給pAn時(shí),C++編譯器進(jìn)行了類型轉(zhuǎn)換,此時(shí)C++編譯器認(rèn)為變量pAn保存的就是animal對象的地址。當(dāng)在main()函數(shù)中執(zhí)行pAn->breathe()時(shí),調(diào)用的當(dāng)然就是animal對象的breathe函數(shù)。
2、 內(nèi)存模型的角度
我們給出了fish對象內(nèi)存模型,如下圖所示:
我們構(gòu)造fish類的對象時(shí),首先要調(diào)用animal類的構(gòu)造函數(shù)去構(gòu)造animal類的對象,然后才調(diào)用fish類的構(gòu)造函數(shù)完成自身部分的構(gòu)造,從而拼接出一個(gè)完整的fish對象。當(dāng)我們將fish類的對象轉(zhuǎn)換為animal類型時(shí),該對象就被認(rèn)為是原對象整個(gè)內(nèi)存模型的上半部分,也就是圖1-1中的“animal的對象所占內(nèi)存”。那么當(dāng)我們利用類型轉(zhuǎn)換后的對象指針去調(diào)用它的方法時(shí),當(dāng)然也就是調(diào)用它所在的內(nèi)存中的方法。因此,輸出animal breathe,也就順理成章了。
正如很多學(xué)員所想,在例1-1的程序中,我們知道pAn實(shí)際指向的是fish類的對象,我們希望輸出的結(jié)果是魚的呼吸方法,即調(diào)用fish類的breathe方法。這個(gè)時(shí)候,就該輪到虛函數(shù)登場了。
前面輸出的結(jié)果是因?yàn)榫幾g器在編譯的時(shí)候,就已經(jīng)確定了對象調(diào)用的函數(shù)的地址,要解決這個(gè)問題就要使用遲綁定(late binding)技術(shù)。當(dāng)編譯器使用遲綁定時(shí),就會(huì)在運(yùn)行時(shí)再去確定對象的類型以及正確的調(diào)用函數(shù)。而要讓編譯器采用遲綁定,就要在基類中聲明函數(shù)時(shí)使用virtual關(guān)鍵字(注意,這是必須的,很多學(xué)員就是因?yàn)闆]有使用虛函數(shù)而寫出很多錯(cuò)誤的例子),這樣的函數(shù)我們稱為虛函數(shù)。一旦某個(gè)函數(shù)在基類中聲明為virtual,那么在所有的派生類中該函數(shù)都是virtual,而不需要再顯式地聲明為virtual。
下面修改例1-1的代碼,將animal類中的breathe()函數(shù)聲明為virtual,如下:
#include <iostream.h>
class animal
{
public:
void sleep()
{
cout<<"animal sleep"<<endl;
}
virtual void breathe()
{
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe()
{
cout<<"fish bubble"<<endl;
}
};
void main()
{
fish fh;
animal *pAn=&fh; // 隱式類型轉(zhuǎn)換
pAn->breathe();
}
大家可以再次運(yùn)行這個(gè)程序,你會(huì)發(fā)現(xiàn)結(jié)果是“fish bubble”,也就是根據(jù)對象的類型調(diào)用了正確的函數(shù)。
那么當(dāng)我們將breathe()聲明為virtual時(shí),在背后發(fā)生了什么呢?
編譯器在編譯的時(shí)候,發(fā)現(xiàn)animal類中有虛函數(shù),此時(shí)編譯器會(huì)為每個(gè)包含虛函數(shù)的類創(chuàng)建一個(gè)虛表(即vtable),該表是一個(gè)一維數(shù)組,在這個(gè)數(shù)組中存放每個(gè)虛函數(shù)的地址。對于例1-2的程序,animal和fish類都包含了一個(gè)虛函數(shù)breathe(),因此編譯器會(huì)為這兩個(gè)類都建立一個(gè)虛表,(即使子類里面沒有virtual函數(shù),但是其父類里面有,所以子類中也有了)如下圖所示:
那么如何定位虛表呢?編譯器另外還為每個(gè)類的對象提供了一個(gè)虛表指針(即vptr),這個(gè)指針指向了對象所屬類的虛表。在程序運(yùn)行時(shí),根據(jù)對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調(diào)用虛函數(shù)時(shí),就能夠找到正確的函數(shù)。對于例1-2的程序,由于pAn實(shí)際指向的對象類型是fish,因此vptr指向的fish類的vtable,當(dāng)調(diào)用pAn->breathe()時(shí),根據(jù)虛表中的函數(shù)地址找到的就是fish類的breathe()函數(shù)。
正是由于每個(gè)對象調(diào)用的虛函數(shù)都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。那么虛表指針在什么時(shí)候,或者說在什么地方初始化呢?
答案是在構(gòu)造函數(shù)中進(jìn)行虛表的創(chuàng)建和虛表指針的初始化。還記得構(gòu)造函數(shù)的調(diào)用順序嗎,在構(gòu)造子類對象時(shí),要先調(diào)用父類的構(gòu)造函數(shù),此時(shí)編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當(dāng)執(zhí)行子類的構(gòu)造函數(shù)時(shí),子類對象的虛表指針被初始化,指向自身的虛表。對于例2-2的程序來說,當(dāng)fish類的fh對象構(gòu)造完畢后,其內(nèi)部的虛表指針也就被初始化為指向fish類的虛表。在類型轉(zhuǎn)換后,調(diào)用pAn->breathe(),由于pAn實(shí)際指向的是fish類的對象,該對象內(nèi)部的虛表指針指向的是fish類的虛表,因此最終調(diào)用的是fish類的breathe()函數(shù)。
要注意:對于虛函數(shù)調(diào)用來說,每一個(gè)對象內(nèi)部都有一個(gè)虛表指針,該虛表指針被初始化為本類的虛表。所以在程序中,不管你的對象類型如何轉(zhuǎn)換,但該對象內(nèi)部的虛表指針是固定的,所以呢,才能實(shí)現(xiàn)動(dòng)態(tài)的對象函數(shù)調(diào)用,這就是C++多態(tài)性實(shí)現(xiàn)的原理。
總結(jié)(基類有虛函數(shù)):
1. 每一個(gè)類都有虛表。
2. 虛表可以繼承,如果子類沒有重寫虛函數(shù),那么子類虛表中仍然會(huì)有該函數(shù)的地址,只不過這個(gè)地址指向的是基類的虛函數(shù)實(shí)現(xiàn)。如果基類有3個(gè)虛函數(shù),那么基類的虛表中就有三項(xiàng)(虛函數(shù)地址),派生類也會(huì)有虛表,至少有三項(xiàng),如果重寫了相應(yīng)的虛函數(shù),那么虛表中的地址就會(huì)改變,指向自身的虛函數(shù)實(shí)現(xiàn)。如果派生類有自己的虛函數(shù),那么虛表中就會(huì)添加該項(xiàng)。
3. 派生類的虛表中虛函數(shù)地址的排列順序和基類的虛表中虛函數(shù)地址排列順序相同。
這就是C++中的多態(tài)性。當(dāng)C++編譯器在編譯的時(shí)候,發(fā)現(xiàn)animal類的breathe()函數(shù)是虛函數(shù),這個(gè)時(shí)候C++就會(huì)采用遲綁定(late binding)技術(shù)。也就是編譯時(shí)并不確定具體調(diào)用的函數(shù),而是在運(yùn)行時(shí),依據(jù)對象的類型(在程序中,我們傳遞的fish類對象的地址)來確認(rèn)調(diào)用的是哪一個(gè)函數(shù),這種能力就叫做C++的多態(tài)性。我們沒有在breathe()函數(shù)前加virtual關(guān)鍵字時(shí),C++編譯器在編譯時(shí)就確定了哪個(gè)函數(shù)被調(diào)用,這叫做早期綁定(early binding)。
C++的多態(tài)性是通過遲綁定技術(shù)來實(shí)現(xiàn)的。
C++的多態(tài)性用一句話概括就是:在基類的函數(shù)前加上virtual關(guān)鍵字,在派生類中重寫該函數(shù),運(yùn)行時(shí)將會(huì)根據(jù)對象的實(shí)際類型來調(diào)用相應(yīng)的函數(shù)。如果對象類型是派生類,就調(diào)用派生類的函數(shù);如果對象類型是基類,就調(diào)用基類的函數(shù)。
虛函數(shù)是在基類中定義的,目的是不確定它的派生類的具體行為。例:
定義一個(gè)基類:class Animal//動(dòng)物。它的函數(shù)為breathe()//呼吸。
再定義一個(gè)類class Fish//魚 。它的函數(shù)也為breathe()
再定義一個(gè)類class Sheep //羊。它的函數(shù)也為breathe()
為了簡化代碼,將Fish,Sheep定義成基類Animal的派生類。
然而Fish與Sheep的breathe不一樣,一個(gè)是在水中通過水來呼吸,一個(gè)是直接呼吸空氣。所以基類不能確定該如何定義breathe,所以在基類中只定義了一個(gè)virtual breathe,它是一個(gè)空的虛函數(shù)。具本的函數(shù)在子類中分別定義。程序一般運(yùn)行時(shí),找到類,如果它有基類,再找它的基類,最后運(yùn)行的是基類中的函數(shù),這時(shí),它在基類中找到的是virtual標(biāo)識的函數(shù),它就會(huì)再回到子類中找同名函數(shù)。派生類也叫子類?;愐步懈割?。這就是虛函數(shù)的產(chǎn)生,和類的多態(tài)性(breathe)的體現(xiàn)。
這里的多態(tài)性是指類的多態(tài)性。
函數(shù)的多態(tài)性是指一個(gè)函數(shù)被定義成多個(gè)不同參數(shù)的函數(shù),它們一般被存在頭文件中,當(dāng)你調(diào)用這個(gè)函數(shù),針對不同的參數(shù),就會(huì)調(diào)用不同的同名函數(shù)。例:Rect()//矩形。它的參數(shù)可以是兩個(gè)坐標(biāo)點(diǎn)(point,point)也可能是四個(gè)坐標(biāo)(x1,y1,x2,y2)這叫函數(shù)的多態(tài)性與函數(shù)的重載。
類的多態(tài)性,是指用虛函數(shù)和延遲綁定來實(shí)現(xiàn)的。函數(shù)的多態(tài)性是函數(shù)的重載。
一般情況下(沒有涉及virtual函數(shù)),當(dāng)我們用一個(gè)指針/引用調(diào)用一個(gè)函數(shù)的時(shí)候,被調(diào)用的函數(shù)是取決于這個(gè)指針/引用的類型。即如果這個(gè)指針/引用是基類對象的指針/引用就調(diào)用基類的方法;如果指針/引用是派生類對象的指針/引用就調(diào)用派生類的方法,當(dāng)然如果派生類中沒有此方法,就會(huì)向上到基類里面去尋找相應(yīng)的方法。這些調(diào)用在編譯階段就確定了。
當(dāng)設(shè)計(jì)到多態(tài)性的時(shí)候,采用了虛函數(shù)和動(dòng)態(tài)綁定,此時(shí)的調(diào)用就不會(huì)在編譯時(shí)候確定而是在運(yùn)行時(shí)確定。不在單獨(dú)考慮指針/引用的類型而是看指針/引用的對象的類型來判斷函數(shù)的調(diào)用,根據(jù)對象中虛指針指向的虛表中的函數(shù)的地址來確定調(diào)用哪個(gè)函數(shù)。
上一篇:函數(shù)外初始化與函數(shù)內(nèi)初始化詳細(xì)解析
欄 目:C語言
下一篇:函數(shù)式宏定義與普通函數(shù)的區(qū)別
本文標(biāo)題:C++多態(tài)的實(shí)現(xiàn)及原理詳細(xì)解析
本文地址:http://mengdiqiu.com.cn/a1/Cyuyan/4050.html
您可能感興趣的文章
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)式函數(shù)庫
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對數(shù)怎么表達(dá)
- 04-02c語言沒有round函數(shù) round c語言
- 04-02C語言中怎么打出三角函數(shù) c語言中怎么打出三角函數(shù)的值
- 01-10c語言求1+2+...+n的解決方法
- 01-10求子數(shù)組最大和的解決方法詳解
- 01-10深入理解約瑟夫環(huán)的數(shù)學(xué)優(yōu)化方法
- 01-10深入二叉樹兩個(gè)結(jié)點(diǎn)的最低共同父結(jié)點(diǎn)的詳解
- 01-10數(shù)據(jù)結(jié)構(gòu)課程設(shè)計(jì)- 解析最少換車次數(shù)的問題詳解
- 01-10c語言 跳臺階問題的解決方法


閱讀排行
本欄相關(guān)
- 04-02c語言函數(shù)調(diào)用后清空內(nèi)存 c語言調(diào)用
- 04-02func函數(shù)+在C語言 func函數(shù)在c語言中
- 04-02c語言的正則匹配函數(shù) c語言正則表達(dá)
- 04-02c語言用函數(shù)寫分段 用c語言表示分段
- 04-02c語言中對數(shù)函數(shù)的表達(dá)式 c語言中對
- 04-02c語言編寫函數(shù)冒泡排序 c語言冒泡排
- 04-02c語言沒有round函數(shù) round c語言
- 04-02c語言分段函數(shù)怎么求 用c語言求分段
- 04-02C語言中怎么打出三角函數(shù) c語言中怎
- 04-02c語言調(diào)用函數(shù)求fibo C語言調(diào)用函數(shù)求
隨機(jī)閱讀
- 08-05dedecms(織夢)副欄目數(shù)量限制代碼修改
- 01-10SublimeText編譯C開發(fā)環(huán)境設(shè)置
- 08-05DEDE織夢data目錄下的sessions文件夾有什
- 01-11Mac OSX 打開原生自帶讀寫NTFS功能(圖文
- 01-10使用C語言求解撲克牌的順子及n個(gè)骰子
- 08-05織夢dedecms什么時(shí)候用欄目交叉功能?
- 01-10C#中split用法實(shí)例總結(jié)
- 01-10delphi制作wav文件的方法
- 04-02jquery與jsp,用jquery
- 01-11ajax實(shí)現(xiàn)頁面的局部加載