n久沒(méi)有寫(xiě)過(guò)了,轉(zhuǎn)載一篇,呵呵。不喜歡轉(zhuǎn)載,但這篇文章確實(shí)還不錯(cuò),只是不知道為什么找不到原文出處。
以下為轉(zhuǎn)載全文。修改了一些細(xì)節(jié)。對(duì)cppblog崩潰了,這是啥所見(jiàn)即所得排版啊,唉,暈倒。
這幾天寫(xiě)的程序應(yīng)用到多繼承。
以前對(duì)多繼承的概念非常清晰,可是很久沒(méi)用就有點(diǎn)模糊了。重新研究一下,“刷新”下記憶。
假設(shè)我們有下面的代碼:
#include <stdio.h>
class A
{
private:
char data;
public:
A(){data = 'A';}
virtual void Show(){printf("A\n");};
virtual void DispA(){printf("a\n");};
};
class B
{
private:
int data;
public:
B(){data = 'B';}
virtual void Show(){printf("B\n");};
virtual void DispB(){printf("b\n");};
};
class C
{
private:
char data;
public:
C(){data = 'C';}
virtual void Show(){printf("C\n");};
virtual void DispC(){printf("c\n");};
};
class D : public A, public B, public C
{
public:
char data;
public:
D(){data = 'D';}
virtual void Show(){printf("D\n");};
virtual void DispD(){printf("d\n");};
};
class E : public D
{
private:
char data;
public:
E(){data = 'E';}
virtual void Show(){printf("E\n");};
virtual void DispE(){printf("e\n");};
};
int main()
{
D *d = new D;
A *a = (A*)d;
B *b = (B*)d;
C *c = (C*)d;;
d->Show();
a->Show();
b->Show();
a->DispA();
b->DispB();
d->DispD();
D *d1 = (D*)a;
d1->Show();
d1->DispD();
D *d2 = (D*)b;
d2->Show();
d2->DispD();
char x = d->data;
return 0;
}
每個(gè)類(lèi)都有兩個(gè)虛擬函數(shù)Show()和DispX()。類(lèi)A,B,C是基本類(lèi),而D是多繼承,最后E又繼承了D。那么對(duì)于類(lèi)E,它的內(nèi)存映像是怎樣的呢?為了解答這個(gè)問(wèn)題,我們回顧一下基本類(lèi)的內(nèi)存映像:
+ --------------+ <- this
+ VTAB +
+ --------------+
+ +
+ Data +
+ +
+ --------------+
如果一個(gè)類(lèi)有虛擬函數(shù),那么它就有虛函數(shù)表(VTAB)。類(lèi)的第一個(gè)單元是一個(gè)指針,指向這個(gè)虛函數(shù)表。如果類(lèi)沒(méi)有虛函數(shù),并且它的祖先(所有父類(lèi))均沒(méi)有虛函數(shù),那么它的內(nèi)存映像和C的結(jié)構(gòu)一樣。所謂虛函數(shù)表就是一個(gè)數(shù)組,每個(gè)單元指向一個(gè)虛函數(shù)地址。
如果類(lèi)Y是類(lèi)X的一個(gè)繼承,那么類(lèi)Y的內(nèi)存映像如下:
+ --------------+ <- this
+ Y 的 VTAB +
+ --------------+
+ +
+ X 的 Data +
+ +
+ --------------+
+ +
+ Y 的 Data +
+ +
+ --------------+
Y的虛函數(shù)表基本和X的相似。如果Y有新的虛函數(shù),那么就在VTAB的末尾加上一個(gè)。如果Y重新定義了原有的虛函數(shù),那么原的指針指向新的函數(shù)入口。這樣無(wú)論是內(nèi)存印象和虛函數(shù)表,Y都和X兼容。這樣當(dāng)執(zhí)行 X* x = (Y*)y;之后,x可以很好的被運(yùn)用,并且可以享受新的虛擬函數(shù)。
現(xiàn)在看多重繼承:
class D : public A, public B, public C
{
....
}
它的內(nèi)存映像如下:
+ --+ -----------------+ 00H <- this
+ + D 的 VTAB +
+ A + -----------------+ 04H
+ + A 的 數(shù)據(jù) +
+ --+ -----------------+ 08H
+ + B 的 VTAB' +
+ B + -----------------+ 0CH
+ + B 的 數(shù)據(jù) +
+ --+ -----------------+ 10H
+ + C 的 VTAB' +
+ C + -----------------+ 14H
+ + C 的 數(shù)據(jù) +
+ --+ -----------------+ 18H
+ D + D 的 數(shù)據(jù) +
+ --+ -----------------+
(因?yàn)閷?duì)齊于雙字,A~D的數(shù)據(jù)雖然只是一個(gè)char,但需要對(duì)齊到DWORD,所以占4字節(jié))
對(duì)于A,它和單繼承沒(méi)有什么兩樣。B和C被簡(jiǎn)單地放在A的后面。如果它們虛函數(shù)在D中被重新定義過(guò)(比如Show函數(shù)),那么它們需要使用新的VTAB,使被重定義的虛函數(shù)指到正確的位置上(這對(duì)于COM或類(lèi)似的技術(shù)是至關(guān)重要的)。最后,D的數(shù)據(jù)被放置到最后面。
對(duì)于E的內(nèi)存映像問(wèn)題就可以不說(shuō)自明了。
下面我們看一下C++是如何處理
D *d;
......
B *b = (B*)d;
這樣的要求的。設(shè)置斷點(diǎn),進(jìn)入反匯編,你可以看到如下的匯編代碼:
B *b = (B*)d;
00401062 cmp dword ptr [d],0
00401066 je main+73h (401073h)
00401068 mov eax,dword ptr [d]
0040106B add eax,8
0040106E mov dword ptr [ebp-38h],eax
00401071 jmp main+7Ah (40107Ah)
00401073 mov dword ptr [ebp-38h],0
0040107A mov ecx,dword ptr [ebp-38h]
0040107D mov dword ptr [b],ecx
從上述匯編代碼可以看出:如果源(這里是d)是NULL,那么目標(biāo)(這里是b)也將被置為NULL,否則目標(biāo)將指向源的地址并向下偏移8個(gè)字節(jié),正好就是上圖所示B的VTAB位置。至于為什么要用ebp-38h作緩存,這是編譯器的算法問(wèn)題了。等以后有時(shí)間再研究。
接下來(lái)看一個(gè)比較古怪的問(wèn)題,這個(gè)也是我寫(xiě)這篇文章的初衷:
根據(jù)上面的多繼承定義,如果給出一個(gè)類(lèi)B的實(shí)例b,我們是否可以求出D的實(shí)例?
為什么要問(wèn)這個(gè)問(wèn)題。因?yàn)榇嬖谙旅娴目赡苄裕?/font>
class B
{
...
virtual int GetTypeID()=0;
...
};
class D : public A, public B, public C
{
...
virtual int GetTypeID(){return 0;};
...
};
class Z : public X, public Y, public B
{
...
virtual int GetTypeID(){return 1;};
...
};
void MyFunc(B* b)
{
int t = b->GetTypeID();
switch(t)
{
case 0:
DoSomething((D*)b); //可能嗎?
break;
case 1:
DoSomething((Z*)b); //可能嗎?
break;
default:
break;
}
}
猛一看很值得懷疑。但仔細(xì)想想,這是可能的,事實(shí)也證明了這一點(diǎn)。因?yàn)榫幾g器了解這D和B這兩個(gè)類(lèi)相互之間的關(guān)系(也就是偏移量),因此它會(huì)做相應(yīng)的轉(zhuǎn)換。同樣,設(shè)置斷點(diǎn),查看匯編:
D *d2 = (D*)b;
00419992 cmp dword ptr [b],0
00419996 je main+196h (4199A6h)
00419998 mov eax,dword ptr [b]
0041999B sub eax,8
0041999E mov dword ptr [ebp-13Ch],eax
004199A4 jmp main+1A0h (4199B0h)
004199A6 mov dword ptr [ebp-13Ch],0
004199B0 mov ecx,dword ptr [ebp-13Ch]
004199B6 mov dword ptr [d2],ecx
如果源(這里是b)為NULL,那么目標(biāo)(這里是d2)也為NULL。否則目標(biāo)取源的地址并向上偏移8個(gè)字節(jié),這樣正好指向D的實(shí)例位置。同樣,為啥需要ebp-13Ch做緩存,待查。
前一段時(shí)間因?yàn)閾?dān)心.NET中將interface轉(zhuǎn)成相應(yīng)的類(lèi)會(huì)有問(wèn)題。今天對(duì)C++多重繼承的復(fù)習(xí)徹底消除了疑云。