C++的多態(tài)性實(shí)現(xiàn)機(jī)制剖析
C++
的多態(tài)性實(shí)現(xiàn)機(jī)制剖析
――即
VC++
視頻第三課
this
指針詳細(xì)說(shuō)明
作者:孫鑫
時(shí)間:
2006
年
1
月
12
日
星期四
1.?? 多態(tài)性和虛函數(shù)
我們先看一個(gè)例子:
例 1- 1
#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;
??????
pAn
->breathe();
}
?????? 注意,在例 1-1 的程序中沒(méi)有定義虛函數(shù)。考慮一下例 1-1 的程序執(zhí)行的結(jié)果是什么?
?????? 答案是輸出: animal breathe
?????? 我們?cè)?/span> main() 函數(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è)方面來(lái)講述原因。
1、? 編譯的角度
C++ 編譯器在編譯的時(shí)候,要確定每個(gè)對(duì)象調(diào)用的函數(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)存模型,如下圖所示:
animal
的對(duì)象所占內(nèi)存
fish
的對(duì)象自身增加的部分
fish
類的對(duì)象所占內(nèi)存
圖 1- 1 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é)果是魚(yú)的呼吸方法,即調(diào)用 fish 類的 breathe 方法。這個(gè)時(shí)候,就該輪到虛函數(shù)登場(chǎng)了。
前面輸出的結(jié)果是因?yàn)榫幾g器在編譯的時(shí)候,就已經(jīng)確定了對(duì)象調(diào)用的函數(shù)的地址,要解決這個(gè)問(wèn)題就要使用遲綁定( late binding )技術(shù)。當(dāng)編譯器使用遲綁定時(shí),就會(huì)在運(yùn)行時(shí)再去確定對(duì)象的類型以及正確的調(diào)用函數(shù)。而要讓編譯器采用遲綁定,就要在基類中聲明函數(shù)時(shí)使用 virtual 關(guān)鍵字(注意,這是必須的,很多學(xué)員就是因?yàn)闆](méi)有使用虛函數(shù)而寫出很多錯(cuò)誤的例子),這樣的函數(shù)我們稱為虛函數(shù)。一旦某個(gè)函數(shù)在基類中聲明為 virtual ,那么在所有的派生類中該函數(shù)都是 virtual ,而不需要再顯式地聲明為 virtual 。
下面修改例 1-1 的代碼,將 animal 類中的 breathe() 函數(shù)聲明為 virtual ,如下:
例 1- 2
#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;
?????? 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è)虛表,如下圖所示:
&animal::breathe()
animal
類的
vtable
animal::breathe
()
&fish::breathe()
fish
類的
vtable
fish::breathe
()
圖 1- 2 animal 類和 fish 類的虛表
?????? 那么如何定位虛表呢?編譯器另外還為每個(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ù)都是通過(guò)虛表指針來(lái)索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說(shuō),在虛表指針沒(méi)有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。那么虛表指針在什么時(shí)候,或者說(shuō)在什么地方初始化呢?
答案是在構(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 的程序來(lái)說(shuō),當(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)用來(lái)說(shuō),每一個(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、? 虛表可以繼承,如果子類沒(méi)有重寫虛函數(shù),那么子類虛表中仍然會(huì)有該函數(shù)的地址,只不過(guò)這個(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ù)地址排列順序相同。
2.?? VC 視頻第三課 this 指針說(shuō)明
我在論壇的 VC 教學(xué)視頻版面發(fā)了帖子,是模擬 MFC 類庫(kù)的例子寫的,主要是說(shuō)明在基類的構(gòu)造函數(shù)中保存的 this 指針是指向子類的,我們?cè)诳匆幌逻@個(gè)例子:
例 1- 3
#include <iostream.h>
class base;
base * pbase;
class base
{
public :
?????? base()
?????? {
????????????? pbase =this;
?????????????
?????? }
?????? virtual void fn()
?????? {
????????????? cout <<"base"<<endl;
?????? }
};
class derived:public base
{
?????? void fn()
?????? {
????????????? cout <<"derived"<<endl;
?????? }
};
derived aa;
void main()
{
?????? pbase ->fn();
}
我在 base 類的構(gòu)造函數(shù)中將 this 指針保存到 pbase 全局變量中。在定義全局對(duì)象 aa ,即調(diào)用 derived aa; 時(shí),要調(diào)用基類的構(gòu)造函數(shù),先構(gòu)造基類的部分,然后是子類的部分,由這兩部分拼接出完整的對(duì)象 aa 。這個(gè) this 指針指向的當(dāng)然也就是 aa 對(duì)象,那么我們?cè)?/span> main() 函數(shù)中利用 pbase 調(diào)用 fn() ,因?yàn)?/span> pbase 實(shí)際指向的是 aa 對(duì)象,而 aa 對(duì)象內(nèi)部的虛表指針指向的是自身的虛表,最終調(diào)用的當(dāng)然是 derived 類中的 fn() 函數(shù)。
在這個(gè)例子中,由于我的疏忽,在 derived 類中聲明 fn() 函數(shù)時(shí),忘了加 public 關(guān)鍵字,導(dǎo)致聲明為了 private (默認(rèn)為 private ),但通過(guò)前面我們所講述的虛函數(shù)調(diào)用機(jī)制,我們也就明白了這個(gè)地方并不影響它輸出正確的結(jié)果。不知道這算不算 C++ 的一個(gè) Bug ,因?yàn)樘摵瘮?shù)的調(diào)用是在運(yùn)行時(shí)確定調(diào)用哪一個(gè)函數(shù),所以編譯器在編譯時(shí),并不知道 pbase 指向的是 aa 對(duì)象,所以導(dǎo)致這個(gè)奇怪現(xiàn)象的發(fā)生。如果你直接用 aa 對(duì)象去調(diào)用,由于對(duì)象類型是確定的(注意 aa 是對(duì)象變量,不是指針變量),編譯器往往會(huì)采用早期綁定,在編譯時(shí)確定調(diào)用的函數(shù),于是就會(huì)發(fā)現(xiàn) fn() 是私有的,不能直接調(diào)用。:)
許多學(xué)員在寫這個(gè)例子時(shí),直接在基類的構(gòu)造函數(shù)中調(diào)用虛函數(shù),前面已經(jīng)說(shuō)了,在調(diào)用基類的構(gòu)造函數(shù)時(shí),編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它只是初始化父類對(duì)象的虛表指針,讓該虛表指針指向父類的虛表,所以你看到結(jié)果當(dāng)然不正確。只有在子類的構(gòu)造函數(shù)調(diào)用完畢后,整個(gè)虛表才構(gòu)建完畢,此時(shí)才能真正應(yīng)用
C++
的多態(tài)性。換句話說(shuō),我們不要在構(gòu)造函數(shù)中去調(diào)用虛函數(shù),當(dāng)然如果你只是想調(diào)用本類的函數(shù),也無(wú)所謂。
3.?? 參考資料:
1 、文章《在 VC6.0 中虛函數(shù)的實(shí)現(xiàn)方法》,作者: backer ,網(wǎng)址:
http://www.mybole.com.cn/bbs/dispbbs.asp?boardid=4&id=1012&star=1
2 、書(shū)《 C++ 編程思想》 機(jī)械工業(yè)出版社
4.?? 后記
本想再寫詳細(xì)些,發(fā)現(xiàn)時(shí)間不夠,總是有很多事情,在加上水平也有限,想想還是以后再說(shuō)吧。不過(guò)我相信,這些內(nèi)容也能夠幫助大家很好的理解了。也歡迎網(wǎng)友能夠繼續(xù)補(bǔ)充,大家可以鼓動(dòng)鼓動(dòng) backer ,讓他從匯編的角度再給一個(gè)說(shuō)明,哈哈,別說(shuō)我說(shuō)的。
posted on 2006-10-09 22:02 weidagang2046 閱讀(553) 評(píng)論(0) 編輯 收藏 所屬分類: C/C++