這幾天寫的程式應用到多繼承。
以前對多繼承的概念非常清晰,可是很久沒用就有點模糊了。重新研究一下,“重新整理”下記憶。
假設我們有下面的代碼:
#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;
}
每個類都有兩個虛擬函數Show()和DispX()。類A,B,C是基本類,而D是多繼承,最後E又繼承了D。那麼對於類E,它的記憶體映像是怎樣的呢?為瞭解答這個問題,我們回顧一下基本類的記憶體映像:
+ --------------+ <- this
+ VTAB +
+ --------------+
+ +
+ Data +
+ +
+ --------------+
如果一個類有虛擬函數,那麼它就有虛函數表(VTAB)。類的第一個單元是一個指標,指向這個虛函數表。如果類沒有虛函數,並且它的祖先(所有父類)均沒有虛函數,那麼它的記憶體映像和C的結構一樣。所謂虛函數表就是一個數組,每個單元指向一個虛函數地址。
如果類Y是類X的一個繼承,那麼類Y的記憶體映像如下:
+ --------------+ <- this
+ Y 的 VTAB +
+ --------------+
+ +
+ X 的 Data +
+ +
+ --------------+
+ +
+ Y 的 Data +
+ +
+ --------------+
Y的虛函數表基本和X的相似。如果Y有新的虛函數,那麼就在VTAB的末尾加上一個。如果Y重新定義了原有的虛函數,那麼原的指標指向新的函數入口。這樣無論是記憶體印象和虛函數表,Y都和X相容。這樣當執行 X* x = (Y*)y;之後,x可以很好的被運用,並且可以享受新的虛擬函數。
現在看多重繼承:
class D : public A, public B, public C
{
....
}
它的記憶體映像如下:
+ --+ -----------------+ 00H <- this
+ + D 的 VTAB +
+ A + -----------------+ 04H
+ + A 的 資料 +
+ --+ -----------------+ 08H
+ + B 的 VTAB' +
+ B + -----------------+ 0CH
+ + B 的 資料 +
+ --+ -----------------+ 10H
+ + C 的 VTAB' +
+ C + -----------------+ 14H
+ + C 的 資料 +
+ --+ -----------------+ 18H
+ D + D 的 資料 +
+ --+ -----------------+
(因為對齊於雙字,A~D的資料雖然只是一個char,但需要對齊到DWORD,所以佔4位元組)
對於A,它和單繼承沒有什麼兩樣。B和C被簡單地放在A的後面。如果它們虛函數在D中被重新定義過(比如Show函數),那麼它們需要使用新的VTAB,使被重定義的虛函數指到正確的位置上(這對於COM或類似的技術是至關重要的)。最後,D的資料被放置到最後面。
對於E的記憶體映像問題就可以不說自明了。
下面我們看一下C++是如何處理
D *d;
......
B *b = (B*)d;
這樣的要求的。設定斷點,進入反組譯碼,你可以看到如下的彙編代碼:(因為UBB關係,將方括弧替換成了大括弧。看上去有點彆扭)
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個位元組,正好就是所示B的VTAB位置。至於為什麼要用ebp-38h作緩衝,這是編譯器的演算法問題了。等以後有時間再研究。
接下來看一個比較古怪的問題,這個也是我寫這篇文章的初衷:
根據上面的多繼承定義,如果給出一個類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這兩個類相互之間的關係(也就是位移量),因此它會做相應的轉換。同樣,設定斷點,查看彙編:
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個位元組,這樣正好指向D的執行個體位置。同樣,為啥需要ebp-13Ch做緩衝,待查。
前一段時間因為擔心.NET中將interface轉成相應的類會有問題。今天對C++多重繼承的複習徹底消除了疑雲。