――即VC++視頻第三課this指針詳細(xì)說明
作者:孫鑫 時(shí)間:2006年1月12日星期四
要更好地理解C++的多態(tài)性,我們需要弄清楚函數(shù)覆蓋的調(diào)用機(jī)制,因此,首先我們介紹一下函數(shù)的覆蓋。
1. 函數(shù)的覆蓋
我們先看一個(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;
pAn->breathe();
注意,在例1-1的程序中沒有定義虛函數(shù)。考慮一下例1-1的程序執(zhí)行的結(jié)果是什么?
答案是輸出:animal breathe
在類fish中重寫了breathe()函數(shù),我們可以稱為函數(shù)的覆蓋。在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è)方面來講述原因。
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)存模型,如下圖所示:

圖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)存中的方法。因此,出現(xiàn)圖2.13所示的結(jié)果,也就順理成章了。
2. 多態(tài)性和虛函數(shù)
正如很多學(xué)員所想,在例1-1的程序中,我們知道pAn實(shí)際指向的是fish類的對(duì)象,我們希望輸出的結(jié)果是魚的呼吸方法,即調(diào)用fish類的breathe方法。這個(gè)時(shí)候,就該輪到虛函數(shù)登場了。
前面輸出的結(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;
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è)虛表,如下圖所示:

圖1- 2 animal類和fish類的虛表
那么如何定位虛表呢?編譯器另外還為每個(gè)類提供了一個(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í)編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它初始化父類的虛表指針,該虛表指針指向父類的虛表。當(dāng)執(zhí)行子類的構(gòu)造函數(shù)時(shí),子類的虛表指針被初始化,指向自身的虛表。對(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ù)地址排列順序相同。
3. VC視頻第三課this指針說明
我在論壇的VC教學(xué)視頻版面發(fā)了帖子,是模擬MFC類庫的例子寫的,主要是說明在基類的構(gòu)造函數(shù)中保存的this指針是指向子類的,我們?cè)诳匆幌逻@個(gè)例子:
#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ì)象,那么我們main()函數(shù)中利用pbase調(diào)用fn(),因?yàn)?FONT face="Times New Roman">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),但通過前面我們所講述的虛函數(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)說了,在調(diào)用基類的構(gòu)造函數(shù)時(shí),編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它只是初始化父類的虛表指針,讓該虛表指針指向父類的虛表,所以你看到結(jié)果當(dāng)然不正確。只有在子類的構(gòu)造函數(shù)調(diào)用完畢后,整個(gè)虛表才構(gòu)建完畢,此時(shí)才能真正應(yīng)用C++的多態(tài)性。換句話說,我們不要在構(gòu)造函數(shù)中去調(diào)用虛函數(shù),當(dāng)然如果你只是想調(diào)用本類的函數(shù),也無所謂。
4. 參考資料:
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、書《C++編程思想》 機(jī)械工業(yè)出版社
5. 后記
本想再寫詳細(xì)些,發(fā)現(xiàn)時(shí)間不夠,總是有很多事情,在加上水平也有限,想想還是以后再說吧。不過我相信,這些內(nèi)容也能夠幫助大家很好的理解了。也歡迎網(wǎng)友能夠繼續(xù)補(bǔ)充,大家可以鼓動(dòng)鼓動(dòng)backer,讓他從匯編的角度再給一個(gè)說明,哈哈,別說我說的。