這幾天寫的程序應用到多繼承。
以前對多繼承的概念非常清晰,可是很久沒用就有點模糊了。重新研究一下,“刷新”下記憶。
假設我們有下面的代碼:
#include <stdio.h>
class A
{
private:
char data;
public:
A(){data = A;}
virtual void Show(){printf("A
");};
virtual void DispA(){printf("a
");};
};
class B
{
private:
int data;
public:
B(){data = B;}
virtual void Show(){printf("B
");};
virtual void DispB(){printf("b
");};
};
class C
{
private:
char data;
public:
C(){data = C;}
virtual void Show(){printf("C
");};
virtual void DispC(){printf("c
");};
};
class D : public A, public B, public C
{
public:
char data;
public:
D(){data = D;}
virtual void Show(){printf("D
");};
virtual void DispD(){printf("d
");};
};
class E : public D
{
private:
char data;
public:
E(){data = E;}
virtual void Show(){printf("E
");};
virtual void DispE(){printf("e
");};
};
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 &nb