n久沒有寫過了,轉(zhuǎn)載一篇,呵呵。不喜歡轉(zhuǎn)載,但這篇文章確實還不錯,只是不知道為什么找不到原文出處。
以下為轉(zhuǎn)載全文。修改了一些細節(jié)。對cppblog崩潰了,這是啥所見即所得排版啊,唉,暈倒。
這幾天寫的程序應用到多繼承。
以前對多繼承的概念非常清晰,可是很久沒用就有點模糊了。重新研究一下,“刷新”下記憶。
假設我們有下面的代碼:
#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;
}
每個類都有兩個虛擬函數(shù)Show()和DispX()。類A,B,C是基本類,而D是多繼承,最后E又繼承了D。那么對于類E,它的內(nèi)存映像是怎樣的呢?為了解答這個問題,我們回顧一下基本類的內(nèi)存映像:
+ --------------+ <- this
+ VTAB +
+ --------------+
+ +
+ Data +
+ +
+ --------------+
如果一個類有虛擬函數(shù),那么它就有虛函數(shù)表(VTAB)。類的第一個單元是一個指針,指向這個虛函數(shù)表。如果類沒有虛函數(shù),并且它的祖先(所有父類)均沒有虛函數(shù),那么它的內(nèi)存映像和C的結構一樣。所謂虛函數(shù)表就是一個數(shù)組,每個單元指向一個虛函數(shù)地址。
如果類Y是類X的一個繼承,那么類Y的內(nèi)存映像如下:
+ --------------+ <- this
+ Y 的 VTAB +
+ --------------+
+ +
+ X 的 Data +
+ +
+ --------------+
+ +
+ Y 的 Data +
+ +
+ --------------+
Y的虛函數(shù)表基本和X的相似。如果Y有新的虛函數(shù),那么就在VTAB的末尾加上一個。如果Y重新定義了原有的虛函數(shù),那么原的指針指向新的函數(shù)入口。這樣無論是內(nèi)存印象和虛函數(shù)表,Y都和X兼容。這樣當執(zhí)行 X* x = (Y*)y;之后,x可以很好的被運用,并且可以享受新的虛擬函數(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ù) +
+ --+ -----------------+
(因為對齊于雙字,A~D的數(shù)據(jù)雖然只是一個char,但需要對齊到DWORD,所以占4字節(jié))
對于A,它和單繼承沒有什么兩樣。B和C被簡單地放在A的后面。如果它們虛函數(shù)在D中被重新定義過(比如Show函數(shù)),那么它們需要使用新的VTAB,使被重定義的虛函數(shù)指到正確的位置上(這對于COM或類似的技術是至關重要的)。最后,D的數(shù)據(jù)被放置到最后面。
對于E的內(nèi)存映像問題就可以不說自明了。
下面我們看一下C++是如何處理
D *d;
......
B *b = (B*)d;
這樣的要求的。設置斷點,進入反匯編,你可以看到如下的匯編代碼:
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,那么目標(這里是b)也將被置為NULL,否則目標將指向源的地址并向下偏移8個字節(jié),正好就是上圖所示B的VTAB位置。至于為什么要用ebp-38h作緩存,這是編譯器的算法問題了。等以后有時間再研究。
接下來看一個比較古怪的問題,這個也是我寫這篇文章的初衷:
根據(jù)上面的多繼承定義,如果給出一個類B的實例b,我們是否可以求出D的實例?
為什么要問這個問題。因為存在下面的可能性:
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;
}
}
猛一看很值得懷疑。但仔細想想,這是可能的,事實也證明了這一點。因為編譯器了解這D和B這兩個類相互之間的關系(也就是偏移量),因此它會做相應的轉(zhuǎ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,那么目標(這里是d2)也為NULL。否則目標取源的地址并向上偏移8個字節(jié),這樣正好指向D的實例位置。同樣,為啥需要ebp-13Ch做緩存,待查。
前一段時間因為擔心.NET中將interface轉(zhuǎn)成相應的類會有問題。今天對C++多重繼承的復習徹底消除了疑云。