當(dāng)我們進(jìn)一步研究類與對(duì)象的時(shí)候,難免的就要考慮到類本身的一些特點(diǎn)以及類與其它類之間的關(guān)系。在本專題開(kāi)始之前,我們已經(jīng)接觸到像一個(gè)類對(duì)象作為另一個(gè)類成員的嵌套關(guān)系了。本專題,我們就專心的研究一下類與類之間的繼承關(guān)系和其類本身的特點(diǎn)。
我們知道,類與對(duì)象的概念是來(lái)自于對(duì)現(xiàn)實(shí)事物的模擬,就像孩子用于其父母的一些特征,不論是木桌還是石桌都有桌子的特點(diǎn)。同樣,類與類之間自然的也應(yīng)該擁有這些特點(diǎn)的。而擁有這些特點(diǎn)就使得我們代碼更加結(jié)構(gòu)化,條理化,最大的好處則是:簡(jiǎn)化我們的代碼,提高代碼的重用性。
好,不多廢話,先讓我們看看,這個(gè)專題大概要講些什么:
1、 體驗(yàn)類的靜態(tài)多態(tài)性之重載
2、 構(gòu)建類與類之間的父子關(guān)系及其訪問(wèn)限制
3、 體驗(yàn)類的動(dòng)態(tài)多態(tài)性之虛函數(shù)
4、 淺析類的多繼承
5、 學(xué)習(xí)小結(jié)
從這個(gè)目錄可以看出這個(gè)專題內(nèi)容非常的關(guān)鍵而且非常的龐雜。本來(lái)我是想將它們分成兩個(gè)專題,分別講述的。可是鑒于它們之間好多的知識(shí)點(diǎn)相互參雜,沒(méi)有辦法很好的分離,為了不給各位讀者遺留困惑,我決定將他們合到一起,希望各位能慢慢體會(huì)其中的奧秘,從根本上掌握它們。
好廢話不多說(shuō),我們進(jìn)入正題。
一、 體驗(yàn)類的靜態(tài)多態(tài)性之重載
重載,當(dāng)時(shí)我理解了半天沒(méi)弄明白是什么意思,現(xiàn)在才知道,就是用模樣相同的東西實(shí)現(xiàn)不同的功能,下面我們分別看一下它們的用法。
1、 函數(shù)重載與缺省參數(shù)
A、函數(shù)重載的實(shí)現(xiàn)原理
假設(shè),我們現(xiàn)在想要寫一個(gè)函數(shù)(如Exp01),它即可以計(jì)算整型數(shù)據(jù)又可以計(jì)算浮點(diǎn)數(shù),那樣我們就得寫兩個(gè)求和函數(shù),對(duì)于更復(fù)雜的情況,我們可能需要寫更多的函數(shù),但是這個(gè)函數(shù)名該怎么起呢?它們本身實(shí)現(xiàn)的功能都差不多,只是針對(duì)不同的參數(shù):
int sum_int(int nNum1, int nNum2)
{
return nNum1 + nNum2;
}
double sum_float(float nNum1, float nNum2)
{
return nNum1 + nNum2;
}
C++中為了簡(jiǎn)化,就引入了函數(shù)重載的概念,大致要求如下:
1、 重載的函數(shù)必須有相同的函數(shù)名
2、 重載的函數(shù)不可以擁有相同的參數(shù)
這樣,我們的函數(shù)就可以寫成:
int sum (int nNum1, int nNum2)
{
return nNum1 + nNum2;
}
double sum (float nNum1, float nNum2)
{
return nNum1 + nNum2;
}
到現(xiàn)在,我們可以考慮一下,它們既然擁有相同的函數(shù)名,那他們?cè)趺磪^(qū)分各個(gè)函數(shù)的呢?相信聰明的你一定根據(jù)上面的要求推測(cè)出來(lái)了,是的,名稱粉碎。很簡(jiǎn)單,我們來(lái)驗(yàn)證一下我的說(shuō)法,繼續(xù)打開(kāi)Exp01工程,點(diǎn)擊菜單欄的”project”à”settings”:

勾選“Generate mapfile”選項(xiàng),然后重新編譯程序,到Debug目錄下找到Exp01.map文件,用記事本打開(kāi)它:
Address
|
Publics by Value
|
Rva+Base
|
Lib:Obj
|
0001:00000050
|
?sum_int@@YAHHH@Z
|
00401050
|
f
|
Exp01.obj
|
0001:00000080
|
?sum_float@@YAMMM@Z
|
00401080
|
f
|
Exp01.obj
|
0001:000000b0
|
?TestCFun@@YAXXZ
|
004010b0
|
f
|
Exp01.obj
|
0001:00000130
|
?sum@@YAHHH@Z
|
00401130
|
f
|
Exp01.obj
|
0001:00000160
|
?sum@@YAMMM@Z
|
00401160
|
f
|
Exp01.obj
|
0001:00000190
|
?TestCplusFun@@YAXXZ
|
00401190
|
f
|
Exp01.obj
|
0001:00000210
|
_main
|
00401210
|
f
|
Exp01.obj
|
哈哈,將參數(shù)信息與函數(shù)名粉碎了并整合成了一個(gè)新的函數(shù)名,今后,我們?cè)诰帉?/span>C++程序的時(shí)候,調(diào)試、排錯(cuò)都難免與這些粉碎后的函數(shù)名打交道,好多的朋友為了解決這個(gè)問(wèn)題想出了各種方法,記得在看雪壇子上有一篇名叫《史上最牛資料助你解惑c++調(diào)試》的文章,大致上把粉碎后的函數(shù)名各部分的含義都解釋出來(lái)了,其實(shí)沒(méi)有這個(gè)必要的,我們用的VC開(kāi)發(fā)環(huán)境中已經(jīng)提供了一個(gè)工具(UNDNAME.EXE),它可以解析這些粉碎后的函數(shù),當(dāng)然如果我們逆向分析它,很容易就可以知道,其實(shí)就是調(diào)用一個(gè)API函數(shù):
UnDecorateSymbolName(m_szFuncName, m_szResultInfo.GetBuffer(0), MAX_PATH,\
UNDNAME_32_BIT_DECODE|UNDNAME_NO_RETURN_UDT_MODEL|\
UNDNAME_NO_MEMBER_TYPE|UNDNAME_NO_THROW_SIGNATURES|\
UNDNAME_NO_THISTYPE|UNDNAME_NO_CV_THISTYPE);
OK,我們隨便輸入粉碎的一個(gè)函數(shù)名看看效果:

B、缺省參數(shù)
如果用Win32API寫過(guò)程序的朋友一定知道,好多的函數(shù)存在許多參數(shù),而且大部分都是NULL,倘若我們有個(gè)函數(shù)大部分的時(shí)候,某個(gè)參數(shù)都是固定值,僅有的時(shí)候需要改變一下,而我們每次調(diào)用它時(shí)都要很費(fèi)勁的輸入?yún)?shù)豈不是很痛苦?C++提供了一個(gè)給參數(shù)加默認(rèn)參數(shù)的功能,例如:
double sum (float nNum1, float nNum2 = 10);
我們調(diào)用時(shí),默認(rèn)情況下,我們只需要給它第一個(gè)參數(shù)傳遞參數(shù)即可,但是使用這個(gè)功能時(shí)需要注意一些事項(xiàng),以免出現(xiàn)莫名其妙的錯(cuò)誤,下面我簡(jiǎn)單的列舉一下大家了解就好。
A、 默認(rèn)參數(shù)只要寫在函數(shù)聲明中即可。
B、 默認(rèn)參數(shù)應(yīng)盡量靠近函數(shù)參數(shù)列表的最右邊,以防止二義性。比如
double sum (float nNum2 = 10,float nNum1);
這樣的函數(shù)聲明,我們調(diào)用時(shí):sum(15);程序就有可能無(wú)法匹配正確的函數(shù)而出現(xiàn)編譯錯(cuò)誤。
2、 淺析運(yùn)算符重載
運(yùn)算符重載也是C++多態(tài)性的基本體現(xiàn),在我們?nèi)粘5木幋a過(guò)程中,我們經(jīng)常進(jìn)行+、—、*、/等操作。在C++中,要想讓我們定義的類對(duì)象也支持這些操作,以簡(jiǎn)化我們的代碼。這就用到了運(yùn)算符重載。
比如,我們要讓一個(gè)日期對(duì)象減去另一個(gè)日期對(duì)象以便得到他們之間的時(shí)間差。再如:我們要讓一個(gè)字符串通過(guò)“+”來(lái)連接另一個(gè)字符串……
要想實(shí)現(xiàn)運(yùn)算符重載,我們一般用到operator關(guān)鍵字,具體用法如下:
返回值 operator 運(yùn)算符(參數(shù)列表)
{
// code
}
例如:
CMyString Operator +(CMyString & csStr)
{
int nTmpLen = strlen(msString.GetData());
if (m_nSpace <= m_nLen+nTmpLen)
{
char *tmpp = new char[m_nLen+nTmpLen+sizeof(char)*2];
strcpy(tmpp, m_szBuffer);
strcat(tmpp, msString.GetData());
delete[] m_szBuffer;
m_szBuffer = tmpp;
}
}
當(dāng)然,運(yùn)算符重載也存在一些限制,在我們編碼的過(guò)程中需要注意一下:
A、 不能使用不存在的運(yùn)算符(如:@、**等等)
B、 “::、. 、.*”運(yùn)算符不可以被重載。
C、 不能改變運(yùn)算符原有的優(yōu)先級(jí)和結(jié)核性。
D、不能改變操作數(shù)的數(shù)量。
E、 只能針對(duì)自定義的類型做重載。
F、 保留運(yùn)算符原本的含義
二、 構(gòu)建類與類之間的父子關(guān)系及其訪問(wèn)限制
1、 繼承代碼的定義方法及其內(nèi)存布局
看代碼:
// 基類
class BaseCls
{
private:
int m_na;
int m_nb;
public:
int GetAValue() const
{
return m_na;
}
int GetBValue() const
{
return m_nb;
}
void SetAValue(int nA);
void SetBValue(int nB);
BaseCls();
~BaseCls();
};
相信大家看明白上面的代碼應(yīng)該是非常容易的。OK,我們繼續(xù):
class SubCls : public BaseCls // 以共有的方式繼承BaseCls
{
private:
int m_nc;
public:
int GetCValue() const
{
return m_nc;
}
void SetCValue(int nC);
SubCls();
~SubCls();
};
OK,倘若我們要給SubCls增加一個(gè)成員函數(shù),來(lái)計(jì)算m_nA+m_nB+m_nC的和:
int SubCls::GetSum()
{
//return m_na+m_nb+m_nc; // 沒(méi)有權(quán)限訪問(wèn)m_na、m_nb
return GetAValue()+ GetBValue() + m_nc;
}
現(xiàn)在我們來(lái)看下它的內(nèi)存結(jié)構(gòu):

由此可見(jiàn),對(duì)于簡(jiǎn)單的繼承關(guān)系,其子類內(nèi)存布局,是先有基類數(shù)據(jù)成員,然后再是子類的數(shù)據(jù)成員,當(dāng)然后面講的復(fù)雜情況,本規(guī)律不一定成立。
2、 關(guān)于繼承權(quán)限的說(shuō)明及其改變
什么時(shí)候SubCls類才能直接使用父類中的變量呢,前人已經(jīng)為我們總結(jié)了一個(gè)張表:
基類訪問(wèn)屬性
繼承權(quán)限
|
public
|
protected
|
private
|
public
|
public
|
protected
|
不可訪問(wèn)
|
protected
|
protected
|
protected
|
不可訪問(wèn)
|
private
|
private
|
private
|
不可訪問(wèn)
|
對(duì)于基類的私有成員,它的子類都無(wú)法直接訪問(wèn),只有通過(guò)相應(yīng)的Get/Set方法來(lái)操作它們(get/set方法必須是public/protected權(quán)限)。換句話說(shuō):基類的private成員雖然不能直接被訪問(wèn),但是他們?cè)谧宇悓?duì)象的內(nèi)存中仍然存在,可以通過(guò)指針來(lái)讀取它們。
當(dāng)一個(gè)類中的成員變量為publi權(quán)限,但它子類繼承它時(shí)采用了priva方式繼承,這樣它就變成了private權(quán)限,我們要讓它繼續(xù)變?yōu)?/span>public權(quán)限,可以對(duì)它重新調(diào)整。比如:
class BaseCls
{
public:
int m_nPub;
};
class SubCls : private BaseCls
{
public:
using BaseCls::b3; // 調(diào)整變量權(quán)限為publi
}
注意,調(diào)整成員訪問(wèn)權(quán)限的前提是:基類成員在子類中是可見(jiàn)的,沒(méi)有被隔離。
三、 體驗(yàn)類的動(dòng)態(tài)多態(tài)性之虛函數(shù)
之前我們講述的多態(tài)性,像函數(shù)重載,運(yùn)算符重載,拷貝構(gòu)造等都是在編譯器完成的多態(tài)性,我們稱之為靜態(tài)多太性,現(xiàn)在我們討論下運(yùn)行期間才體現(xiàn)出來(lái)的多態(tài)性——?jiǎng)討B(tài)多態(tài)性。我將分成幾個(gè)不同的小結(jié),逐步深入的描述我對(duì)虛函數(shù)的理解。
1、 什么是虛函數(shù),用在什么場(chǎng)合
之所以稱為虛函數(shù),是因?yàn)榇祟惡瘮?shù)在被調(diào)用之前誰(shuí)都不確定它會(huì)被誰(shuí)調(diào)用。換句話說(shuō)就是:調(diào)用虛函數(shù)的方式不同于以往的立即數(shù)直接尋址,而是采用了寄存器間接尋址的方式,因此它調(diào)用的地址并不固定。所以,虛函數(shù)可以通過(guò)相同的函數(shù)實(shí)現(xiàn)不同的功能。這便是虛函數(shù)的特點(diǎn)。
我可以舉個(gè)例子來(lái)說(shuō)明虛函數(shù)的用途:
假如,我們有一個(gè)家具類,他有一個(gè)成員函數(shù)來(lái)獲取家具的價(jià)格,如果家具類派生出了桌子、椅子、床、沙發(fā)……各種對(duì)象不計(jì)其數(shù)。這時(shí),如果我們要實(shí)現(xiàn)一個(gè)函數(shù)來(lái)輸出用戶指定“家具”的價(jià)格,我想最常規(guī)的做法應(yīng)該是用一個(gè)很深的if……else if ……else結(jié)構(gòu),當(dāng)然,如果你看過(guò)我寫的switch的學(xué)習(xí)筆記的話,你或許會(huì)用一個(gè)很大的switch結(jié)構(gòu)來(lái)判斷用戶選擇了那個(gè)家具,然后創(chuàng)建相應(yīng)的對(duì)象,調(diào)用其獲取價(jià)格的方法,打印輸出……
當(dāng)然,如果有虛函數(shù),我們就不需要這樣費(fèi)事的寫程序了,我們大可以創(chuàng)建一個(gè)家具類的對(duì)象指針,然后讓它直接指向用戶選擇的家具對(duì)象,當(dāng)用戶選擇“家具”時(shí),我們不需要確定用戶選擇的是哪個(gè)“家具”,只需要簡(jiǎn)單的調(diào)用基類的虛函數(shù)即可。程序在運(yùn)行時(shí),會(huì)自動(dòng)的根據(jù)用戶的不同選擇調(diào)用不同子類的虛函數(shù)……
2、 怎樣使用虛函數(shù)
說(shuō)了一堆的廢話,或許你真的知道虛函數(shù)是怎么回事了,或許你一定很好奇虛函數(shù)是怎么實(shí)現(xiàn)的,也或許,你可能更加疑惑:到底什么是虛函數(shù)了。
都沒(méi)關(guān)系,我們先帶著這些問(wèn)題,一步步的來(lái),先看看如何定義和使用一個(gè)虛函數(shù)以簡(jiǎn)化我們的程序。
要使用虛函數(shù),異常的簡(jiǎn)單,你只要在要制定為虛函數(shù)的聲明前加上Virtual關(guān)鍵字,當(dāng)然,在使用的過(guò)程中需要遵循如下規(guī)則:
a) 虛函數(shù)必須在繼承的情況下使用。
b) 若基類中有一個(gè)虛函數(shù),那它所派生的所有子類中所有函數(shù)名、參數(shù)、返回值都相同的成員方法都是虛函數(shù),不論它們的聲明前是否有Virtual關(guān)鍵字。
c) 只有類的成員方法才能聲明為虛函數(shù),但是:靜態(tài)、內(nèi)聯(lián)、構(gòu)造除外。
d) 析構(gòu)函數(shù)建議聲明為虛函數(shù)。
e) 調(diào)用虛函數(shù)時(shí),必須要通過(guò)基類對(duì)象的指針或者引用來(lái)完成調(diào)用,否則無(wú)法發(fā)揮虛函數(shù)的特性。
如:下來(lái)程序(見(jiàn)Exp03的代碼)
class CBaseCls
{
public:
int m_nBaseData;
virtual void fun(int x)
{
printf("%d in BaseCls...\r\n", x);
}
};
class CSubCls : public CBaseCls
{
public:
int m_nSubData;
// 由于它的參數(shù)返回值還有函數(shù)名等都與父類的虛函數(shù)相同,所以它也是虛函數(shù)。
void fun(int x)
{
printf("%d in subclass...\r\n", x);
}
};
相信上面的代碼,你應(yīng)該能看明白吧,我們按照上面列出的規(guī)范,使用一下這兩個(gè)類:
int main(int argc, char* argv[])
{
//CSubCls *pSub = (CSubCls*)new CBaseCls;
//pSub->fun(5);
CBaseCls objBase;
CSubCls objSub;
CBaseCls *pBase = new CSubCls;
pBase->fun(5);
return 0;
}
運(yùn)行結(jié)果:

3、 虛函數(shù)的運(yùn)行機(jī)制
OK,我們調(diào)試一下這段代碼,看看虛函數(shù)的這鐘特性到底是怎么實(shí)現(xiàn)的:
32: CBaseCls objBase;
0040EDDD lea ecx,[ebp-14h] ; CBaseCls的this指針
0040EDE0 call @ILT+15(CBaseCls::CBaseCls) (00401014)
33: CSubCls objSub;
0040EDE5 lea ecx,[ebp-20h]
0040EDE8 call @ILT+0(CSubCls::CSubCls) (00401005)
34:
35: CBaseCls *pBase = new CSubCls;
0040EDED push 0Ch
0040EDEF call operator new (00401350) ; new一個(gè)空間
0040EDF4 add esp,4
0040EDF7 mov dword ptr [ebp-2Ch],eax
0040EDFA mov dword ptr [ebp-4],0
0040EE01 cmp dword ptr [ebp-2Ch],0
0040EE05 je main+64h (0040ee14)
0040EE07 mov ecx,dword ptr [ebp-2Ch] ; 子類的this
0040EE0A call @ILT+0(CSubCls::CSubCls) (00401005) ; 調(diào)用子類構(gòu)造創(chuàng)建子類的臨時(shí)對(duì)象
0040EE0F mov dword ptr [ebp-70h],eax ; 保存子類的this指針
0040EE12 jmp main+6Bh (0040ee1b)
0040EE14 mov dword ptr [ebp-70h],0
0040EE1B mov eax,dword ptr [ebp-70h] ; 子類的this
0040EE1E mov dword ptr [ebp-28h],eax ; 子類的this
0040EE21 mov dword ptr [ebp-4],0FFFFFFFFh
0040EE28 mov ecx,dword ptr [ebp-28h] ; 子類的this
0040EE2B mov dword ptr [ebp-24h],ecx ; 可見(jiàn),[ebp-24h]中是子類的this
36: pBase->fun(5);
0040EE2E mov esi,esp
0040EE30 push 5
0040EE32 mov eax,dword ptr [ebp-24h]
0040EE35 mov edx,dword ptr [eax] ; this指針去內(nèi)容(也就是虛函數(shù)指針表簡(jiǎn)稱虛表)
0040EE37 mov ecx,dword ptr [ebp-24h] ; 傳遞this指針
0040EE3A call dword ptr [edx] ; 調(diào)用虛表的第0項(xiàng)函數(shù)
0040EE3C cmp esi,esp
這時(shí),我們應(yīng)該發(fā)現(xiàn),我們對(duì)象的內(nèi)存結(jié)構(gòu)應(yīng)該是這樣的(比以前的內(nèi)存結(jié)構(gòu)多了個(gè)虛函數(shù)表的指針):

由此可知,this指針指向一個(gè)函數(shù)表的首地址,這個(gè)表的每一項(xiàng)都是一個(gè)函數(shù)地址(函數(shù)指針),換句話說(shuō),虛函數(shù)的指針都被存放在this指向的這個(gè)虛函數(shù)表中。
現(xiàn)在,我們?cè)倩仡^看上述的程序,猜測(cè)一下他的編譯過(guò)程。
A、 編譯父類時(shí),先編譯代碼,最后把虛函數(shù)的首地址加入到虛函數(shù)表中。并將虛表的首地址作為本類的第一個(gè)數(shù)據(jù)成員
B、 編譯子類時(shí),先編譯子類的代碼,再把父類的虛函數(shù)表拷貝過(guò)來(lái),檢查子類中重新實(shí)現(xiàn)了那些虛函數(shù),一次在子類的虛表中將重新實(shí)現(xiàn)的虛函數(shù)表項(xiàng)覆蓋掉并增加子類中心實(shí)現(xiàn)的虛函數(shù)地址。
那它的執(zhí)行過(guò)程已經(jīng)是可以明白的說(shuō)明了:
A、 我們先分析下上述代碼的執(zhí)行:
CBaseCls *pBase = new CSubCls; // 讓一個(gè)父類指針指向子類的實(shí)例
pBase->fun(5); //調(diào)用虛函數(shù)時(shí),傳遞的是子類的this指針,也就是子類的虛表
再根據(jù)我們上面的分析,很自然的,代碼將調(diào)用子類的函數(shù),輸出的結(jié)果也自然的是子類虛函數(shù)的結(jié)果。
B、 倘若,我們將調(diào)用的代碼換一下:用子類的指針指向父類的實(shí)例。
CSubCls *pSub = new CBaseCls; // 編譯出錯(cuò),需要強(qiáng)轉(zhuǎn)
pSub ->fun(5); //輸出的是父類的虛函數(shù)的結(jié)果
編譯器不支持父類對(duì)象向子類類型的轉(zhuǎn)換,我們想一下它們的內(nèi)存結(jié)構(gòu)就可以知道,一般子類的數(shù)據(jù)成員比父類的多,父類想子類轉(zhuǎn)換以后,其指針取成員會(huì)存在安全隱患。
C、 由此,我們可以得出結(jié)論:
a) 調(diào)用虛函數(shù)時(shí),傳遞不同類實(shí)例的this指針,就調(diào)用傳遞this對(duì)象的虛函數(shù)。
b) 虛函數(shù)的調(diào)用方式:
用基類的指針或引用指向子類的實(shí)例,通過(guò)基類的指針調(diào)用子類的虛函數(shù)。
說(shuō)明:一定要用指針或引用去調(diào)用虛函數(shù),否則可能會(huì)失去虛函數(shù)的特性。
4、 模擬實(shí)現(xiàn)虛函數(shù)機(jī)制
到這里,我想你一定對(duì)虛函數(shù)有一定的了解了,為了加深印象,我們不妨手工用C語(yǔ)言類模擬一個(gè)虛函數(shù)出來(lái)(代碼見(jiàn)Exp04):
#include <stdio.h>
// 定義函數(shù)指針
class CPerson;
typedef void (*PFUN_TYPE)();
typedef void (CPerson::*PBASEFUN_TYPE)();
// 基類的成員。
class CPerson
{
public:
PBASEFUN_TYPE *m_pFunPoint;//定義函數(shù)指針
CPerson()
{
m_pFunPoint = (PBASEFUN_TYPE*)new PFUN_TYPE[2]; // 保存虛函數(shù)指針表
m_pFunPoint[0] = (PBASEFUN_TYPE)vsayHello; // 填充虛表項(xiàng)
m_pFunPoint[1] = (PBASEFUN_TYPE)vsayGoodbye;
}
~CPerson()
{
// 釋放資源,防止內(nèi)存泄露
delete [] m_pFunPoint;
}
void sayHello()
{
printf("person::Hello\r\n");
}
void sayGoodbye()
{
printf("person::Goodbye\r\n");
}
void vsayHello()
{
sayHello();
}
void vsayGoodbye()
{
sayGoodbye();
}
};
class CStudent:public CPerson
{
public:
CStudent()
{
// 填充虛表項(xiàng),覆蓋父類的成員地址
m_pFunPoint[0] = (PBASEFUN_TYPE)vsayHello;
m_pFunPoint[1] = (PBASEFUN_TYPE)vsayGoodbye;
}
void sayHello()
{
printf("CStudent::Hello\r\n");
}
void sayGoodbye()
{
printf("CStudent::Goodbye\r\n");
}
void vsayHello()
{
sayHello();
}
void vsayGoodbye()
{
sayGoodbye();
}
};
int main()
{
CStudent objStu;
CPerson objPer;
CPerson *pobjPer = &objStu; // 用基類指針指向子類對(duì)象
objPer.vsayHello(); // 用基類對(duì)象直接調(diào)用
objPer.vsayGoodbye();
(pobjPer->*pobjPer->m_pFunPoint[0])(); // 用基類指針調(diào)用
(pobjPer->*pobjPer->m_pFunPoint[1])();
return 0;
}
運(yùn)行結(jié)果:


四、 淺析類的多繼承
一個(gè)類可以從多個(gè)基類中派生,也就是說(shuō):一個(gè)類可以同時(shí)擁有多個(gè)類的特性,是的,他有多個(gè)基類。這樣的繼承結(jié)構(gòu)叫作“多繼承”,最典型的例子就是 沙發(fā)-床了:

1、 基本概念
相信上圖描述的結(jié)構(gòu)大家應(yīng)該都可以看明白的,SleepSofa類繼承自Bed和Sofa兩個(gè)類,因此,SleepSofa類擁有這兩個(gè)類的特性,但在實(shí)際編碼中會(huì)存在如下幾個(gè)問(wèn)題。
a) SleepSofa類該如何定義?
Class SleepSofa : public Bed, public Sofa
{
….
}
構(gòu)造順序?yàn)椋?/span>Bed à sofa à sleepsofa (也就是書(shū)寫的順序)
b) Bed和Sofa類中都有Weight屬性頁(yè)都有GetWeight和SetWeight方法,在SleepSofa類中使用這些屬性和方法時(shí),如何確定調(diào)用的是哪個(gè)類的成員?
可以使用完全限定名的方式,比如:
Sleepsofa objsofa;
Objsofa.Bed::SetWeight(); // 給方法加上一個(gè)作用域,問(wèn)題就解決了。
2、 虛繼承
上節(jié)對(duì)多繼承作了大概的描述,相信大家對(duì)SleepSofa類有了大概的認(rèn)識(shí),我們回頭仔細(xì)看下Furniture類:
倘若,我們定義一個(gè)SleepSofa對(duì)象,讓我們分析一下它的構(gòu)造過(guò)程:它會(huì)構(gòu)造Bed類和Sofa類,但Bed類和Sofa類都有一個(gè)父類,因此Furniture類被構(gòu)造了兩次,這是不合理的,因此,我們引入了虛繼承的概念。
class Furniture{……};
class Bed : virtual public Furniture{……}; // 這里我們使用虛繼承
class Sofa : virtual public Furniture{……};// 這里我們使用虛繼承
class sleepSofa : public Bed, public Sofa {……};
這樣,Furniture類就之構(gòu)造一次了……
3、 總結(jié)下繼承情況中子類對(duì)象的內(nèi)存結(jié)構(gòu)
A. 單繼承情況下子類實(shí)例的內(nèi)存結(jié)構(gòu)
// 描述單繼承情況下子類實(shí)例的內(nèi)存結(jié)構(gòu)
#include "stdafx.h"
class A
{
public:
A(){m_A = 0;}
virtual fun1(){};
int m_A;
};
class B:public A
{
public:
B(){m_B = 1;}
virtual fun1(){};
virtual fun2(){};
int m_B;
};
int main(int argc, char* argv[])
{
B* pB = new B;
return 0;
}

B. 多繼承情況下子類實(shí)例的內(nèi)存結(jié)構(gòu)
// 描述多繼承情況下子類實(shí)例的內(nèi)存結(jié)構(gòu)
#include "stdafx.h"
#include <stdio.h>
class A
{
public:
A(){m_A = 1;};
~A(){};
virtual int funA(){printf("in funA\r\n"); return 0;};
int m_A;
};
class B
{
public:
B(){m_B = 2;};
~B(){};
virtual int funB(){printf("in funB\r\n"); return 0;};
int m_B;
};
class C
{
public:
C(){m_C = 3;};
~C(){};
virtual int funC(){printf("in funC\r\n"); return 0;};
int m_C;
};
class D:public A,public B,public C
{
public:
D(){m_D = 4;};
~D(){};
virtual int funD(){printf("in funD\r\n"); return 0;};
int m_D;
};

C. 部分虛繼承的情況下子類實(shí)例的內(nèi)存結(jié)構(gòu)
// 描述部分虛繼承的情況下,子類實(shí)例的內(nèi)存結(jié)構(gòu)
#include "stdafx.h"
class A
{
public:
A(){m_A = 0;};
virtual funA(){};
int m_A;
};
class B
{
public:
B(){m_B = 1;};
virtual funB(){};
int m_B;
};
class C
{
public:
C(){m_C = 2;};
virtual funC(){};
int m_C;
};
class D:virtual public A,public B,public C
{
public:
D(){m_D = 3;};
virtual funD(){};
int m_D;
};
int main(int argc, char* argv[])
{
D* pD = new D;
return 0;
}

D. 全部虛繼承的情況下,子類實(shí)例的內(nèi)存結(jié)構(gòu)
// 描述全部虛繼承的情況下,子類實(shí)例的內(nèi)存結(jié)構(gòu)
#include "stdafx.h"
class A
{
public:
A(){m_A = 0;}
virtual funA(){};
int m_A;
};
class B
{
public:
B(){m_B = 1;}
virtual funB(){};
int m_B;
};
class C:virtual public A,virtual public B
{
public:
C(){m_C = 2;}
virtual funC(){};
int m_C;
};
int main(int argc, char* argv[])
{
C* pC = new C;
return 0;
}

E. 菱形結(jié)構(gòu)繼承關(guān)系下子類實(shí)例的內(nèi)存結(jié)構(gòu)
// 描述菱形結(jié)構(gòu)繼承關(guān)系下子類實(shí)例的內(nèi)存結(jié)構(gòu)
#include "stdafx.h"
class A
{
public:
A(){m_A = 0;}
virtual funA(){};
int m_A;
};
class B :virtual public A
{
public:
B(){m_B = 1;}
virtual funB(){};
int m_B;
};
class C :virtual public A
{
public:
C(){m_C = 2;}
virtual funC(){};
int m_C;
};
class D: public B, public C
{
public:
D(){m_D = 3;}
virtual funD(){};
int m_D;
};
int main(int argc, char* argv[])
{
D* pD = new D;
return 0;
}

上圖中,多出兩個(gè)未知的地址,它們并不是類成員的地址,如果我們跟蹤它,得到的結(jié)果如下圖:

如果留意觀察這兩個(gè)地址,就會(huì)發(fā)現(xiàn),它們都緊跟著虛繼承的兩個(gè)子類:
A、00425024 à 是B類的虛表指針,B類虛繼承與A類
B、00425020 à 是C類的虛表指針,C類虛繼承與A類
知道了這兩點(diǎn),我們先跟蹤0x00425030這個(gè)地址,我們得到一個(gè)-4和0x0C兩個(gè)數(shù),這兩個(gè)數(shù)字很難不讓我們想到這個(gè)是偏移。
0x00425030 所在的位置減去4,就是C類的虛表指針。
0x00425030 所在的位置加上C,就是A類的虛表指針。
同理,我們?cè)诳?/span>3C這個(gè)位置:
0x0042503C 所在的位置減去4,就是B類的虛表指針。
0x0042503C 所在的位置加上0x18,就是A類的虛表指針
由此,我們可以大膽的猜測(cè),這個(gè)偏移表是用來(lái)關(guān)聯(lián)虛繼承的基類和子類的,比如:
0x00425030這個(gè)偏移表,將A類和C類的虛繼承關(guān)系聯(lián)系到了一起,0x0042503C這個(gè)偏移表則是把A類和B類聯(lián)系到了一起。
4、 總結(jié)下多繼承情況下對(duì)象的構(gòu)造順序
A. 虛繼承的構(gòu)造函數(shù)按照被繼承的順序先構(gòu)造。
B. 非虛繼承的構(gòu)造函數(shù)按照被繼承的順序再構(gòu)造。
C. 成員對(duì)象的構(gòu)造函數(shù)按照聲明順序構(gòu)造。
D. 類自己的構(gòu)造函數(shù)最后構(gòu)造。
五、 學(xué)習(xí)小結(jié)
本專題本來(lái)是想分成兩個(gè)專題分別講述類的繼承和多態(tài)性的。可是由于這兩個(gè)特性聯(lián)系的實(shí)在是太緊密,這樣穿插在一起,我著實(shí)沒(méi)想到更好的分類方法。索性將這兩個(gè)特性放在一個(gè)專題中一并講述。
但是我的言語(yǔ)表達(dá)能力實(shí)在是有限,總是不能把文章中的知識(shí)點(diǎn)講述到我想象中的那樣簡(jiǎn)單、清晰,這個(gè)專題,不求能教給大家點(diǎn)什么,只希望在大家的學(xué)習(xí)過(guò)程中能起到墊腳石的作用。我就心滿意足了。
本專題的內(nèi)容可以說(shuō)是C++語(yǔ)言的精華所在,講述的知識(shí)雖然重要,但也僅限于我現(xiàn)在這個(gè)知識(shí)層面上的理解,或許過(guò)些日子又會(huì)發(fā)現(xiàn)更多更重要的知識(shí)我沒(méi)有講到或者講的不對(duì)……
如果你在閱讀本文章個(gè)過(guò)程中,如果發(fā)現(xiàn)我哪里講的不對(duì),麻煩通知我,以便我及時(shí)改正,以免誤人子弟……
—— besterChen
2010年5月20日星期四