weidagang2046的專欄

          物格而后知致
          隨筆 - 8, 文章 - 409, 評(píng)論 - 101, 引用 - 0
          數(shù)據(jù)加載中……

          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++

          主站蜘蛛池模板: 调兵山市| 屯门区| 临洮县| 驻马店市| 定结县| 始兴县| 怀仁县| 钦州市| 和静县| 达拉特旗| 海阳市| 海晏县| 淮安市| 偃师市| 平邑县| 邻水| 旅游| 轮台县| 河池市| 临邑县| 金门县| 焉耆| 商河县| 黄骅市| 迭部县| 梅州市| 玉环县| 客服| 东阿县| 合阳县| 鸡西市| 晴隆县| 秦皇岛市| 武胜县| 同仁县| 搜索| 屏边| 铜山县| 伊春市| 开鲁县| 木兰县|