C++多態(tài)的實(shí)現(xiàn)及原理詳細(xì)解析
1. 用virtual關(guān)鍵字申明的函數(shù)叫做虛函數(shù),虛函數(shù)肯定是類的成員函數(shù)。
2. 存在虛函數(shù)的類都有一個(gè)一維的虛函數(shù)表叫做虛表。類的對(duì)象有一個(gè)指向虛表開始的虛指針。虛表是和類對(duì)應(yīng)的,虛表指針是和對(duì)象對(duì)應(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
我們?cè)趍ain()函數(shù)中首先定義了一個(gè)fish類的對(duì)象fh,接著定義了一個(gè)指向animal類的指針變量pAn,將fh的地址賦給了指針變量pAn,然后利用該變量調(diào)用pAn->breathe()。許多學(xué)員往往將這種情況和C++的多態(tài)性搞混淆,認(rèn)為fh實(shí)際上是fish類的對(duì)象,應(yīng)該是調(diào)用fish類的breathe(),輸出“fish bubble”,然后結(jié)果卻不是這樣。下面我們從兩個(gè)方面來講述原因。
1、 編譯的角度
C++編譯器在編譯的時(shí)候,要確定每個(gè)對(duì)象調(diào)用的函數(shù)(要求此函數(shù)是非虛函數(shù))的地址,這稱為早期綁定(early binding),當(dāng)我們將fish類的對(duì)象fh的地址賦給pAn時(shí),C++編譯器進(jìn)行了類型轉(zhuǎn)換,此時(shí)C++編譯器認(rèn)為變量pAn保存的就是animal對(duì)象的地址。當(dāng)在main()函數(shù)中執(zhí)行pAn->breathe()時(shí),調(diào)用的當(dāng)然就是animal對(duì)象的breathe函數(shù)。
2、 內(nèi)存模型的角度
我們給出了fish對(duì)象內(nèi)存模型,如下圖所示:
我們構(gòu)造fish類的對(duì)象時(shí),首先要調(diào)用animal類的構(gòu)造函數(shù)去構(gòu)造animal類的對(duì)象,然后才調(diào)用fish類的構(gòu)造函數(shù)完成自身部分的構(gòu)造,從而拼接出一個(gè)完整的fish對(duì)象。當(dāng)我們將fish類的對(duì)象轉(zhuǎn)換為animal類型時(shí),該對(duì)象就被認(rèn)為是原對(duì)象整個(gè)內(nèi)存模型的上半部分,也就是圖1-1中的“animal的對(duì)象所占內(nèi)存”。那么當(dāng)我們利用類型轉(zhuǎn)換后的對(duì)象指針去調(diào)用它的方法時(shí),當(dāng)然也就是調(diào)用它所在的內(nèi)存中的方法。因此,輸出animal breathe,也就順理成章了。
正如很多學(xué)員所想,在例1-1的程序中,我們知道pAn實(shí)際指向的是fish類的對(duì)象,我們希望輸出的結(jié)果是魚的呼吸方法,即調(diào)用fish類的breathe方法。這個(gè)時(shí)候,就該輪到虛函數(shù)登場(chǎng)了。
前面輸出的結(jié)果是因?yàn)榫幾g器在編譯的時(shí)候,就已經(jīng)確定了對(duì)象調(diào)用的函數(shù)的地址,要解決這個(gè)問題就要使用遲綁定(late binding)技術(shù)。當(dāng)編譯器使用遲綁定時(shí),就會(huì)在運(yùn)行時(shí)再去確定對(duì)象的類型以及正確的調(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ù)對(duì)象的類型調(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ù)的地址。對(duì)于例1-2的程序,animal和fish類都包含了一個(gè)虛函數(shù)breathe(),因此編譯器會(huì)為這兩個(gè)類都建立一個(gè)虛表,(即使子類里面沒有virtual函數(shù),但是其父類里面有,所以子類中也有了)如下圖所示:
那么如何定位虛表呢?編譯器另外還為每個(gè)類的對(duì)象提供了一個(gè)虛表指針(即vptr),這個(gè)指針指向了對(duì)象所屬類的虛表。在程序運(yùn)行時(shí),根據(jù)對(duì)象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調(diào)用虛函數(shù)時(shí),就能夠找到正確的函數(shù)。對(duì)于例1-2的程序,由于pAn實(shí)際指向的對(duì)象類型是fish,因此vptr指向的fish類的vtable,當(dāng)調(diào)用pAn->breathe()時(shí),根據(jù)虛表中的函數(shù)地址找到的就是fish類的breathe()函數(shù)。
正是由于每個(gè)對(duì)象調(diào)用的虛函數(shù)都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。那么虛表指針在什么時(shí)候,或者說在什么地方初始化呢?
答案是在構(gòu)造函數(shù)中進(jìn)行虛表的創(chuàng)建和虛表指針的初始化。還記得構(gòu)造函數(shù)的調(diào)用順序嗎,在構(gòu)造子類對(duì)象時(shí),要先調(diào)用父類的構(gòu)造函數(shù),此時(shí)編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它初始化父類對(duì)象的虛表指針,該虛表指針指向父類的虛表。當(dāng)執(zhí)行子類的構(gòu)造函數(shù)時(shí),子類對(duì)象的虛表指針被初始化,指向自身的虛表。對(duì)于例2-2的程序來說,當(dāng)fish類的fh對(duì)象構(gòu)造完畢后,其內(nèi)部的虛表指針也就被初始化為指向fish類的虛表。在類型轉(zhuǎn)換后,調(diào)用pAn->breathe(),由于pAn實(shí)際指向的是fish類的對(duì)象,該對(duì)象內(nèi)部的虛表指針指向的是fish類的虛表,因此最終調(diào)用的是fish類的breathe()函數(shù)。
要注意:對(duì)于虛函數(shù)調(diào)用來說,每一個(gè)對(duì)象內(nèi)部都有一個(gè)虛表指針,該虛表指針被初始化為本類的虛表。所以在程序中,不管你的對(duì)象類型如何轉(zhuǎn)換,但該對(duì)象內(nèi)部的虛表指針是固定的,所以呢,才能實(shí)現(xiàn)動(dòng)態(tài)的對(duì)象函數(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ù)對(duì)象的類型(在程序中,我們傳遞的fish類對(duì)象的地址)來確認(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ù)對(duì)象的實(shí)際類型來調(diào)用相應(yīng)的函數(shù)。如果對(duì)象類型是派生類,就調(diào)用派生類的函數(shù);如果對(duì)象類型是基類,就調(diào)用基類的函數(shù)。
虛函數(shù)是在基類中定義的,目的是不確定它的派生類的具體行為。例:
定義一個(gè)基類:class Animal//動(dòng)物。它的函數(shù)為breathe()//呼吸。
再定義一個(gè)類class Fish//魚 。它的函數(shù)也為breathe()
再定義一個(gè)類class Sheep //羊。它的函數(shù)也為breathe()
為了簡(jiǎn)化代碼,將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í)的函數(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ù),針對(duì)不同的參數(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è)指針/引用是基類對(duì)象的指針/引用就調(diào)用基類的方法;如果指針/引用是派生類對(duì)象的指針/引用就調(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ú)考慮指針/引用的類型而是看指針/引用的對(duì)象的類型來判斷函數(shù)的調(diào)用,根據(jù)對(duì)象中虛指針指向的虛表中的函數(shù)的地址來確定調(diào)用哪個(gè)函數(shù)。
相關(guān)文章
C語言實(shí)現(xiàn)猜數(shù)字小項(xiàng)目
這篇文章主要為大家詳細(xì)介紹了C語實(shí)現(xiàn)猜數(shù)字小項(xiàng)目,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01詳解C++ 拷貝構(gòu)造函數(shù)和賦值運(yùn)算符
本文主要介紹了拷貝構(gòu)造函數(shù)和賦值運(yùn)算符的區(qū)別,以及在什么時(shí)候調(diào)用拷貝構(gòu)造函數(shù)、什么情況下調(diào)用賦值運(yùn)算符。最后,簡(jiǎn)單的分析了下深拷貝和淺拷貝的問題。有需要的朋友可以看下2016-12-12C++字符數(shù)組的輸入輸出和字符串結(jié)束標(biāo)志使用講解
這篇文章主要介紹了C++字符數(shù)組的輸入輸出和符串結(jié)束標(biāo)志使用講解,是C++入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-09-09c++創(chuàng)建二維動(dòng)態(tài)數(shù)組與內(nèi)存釋放問題
這篇文章主要介紹了c++創(chuàng)建二維動(dòng)態(tài)數(shù)組與內(nèi)存釋放問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06