@H_502_5@
假设我们有下面的代码:
#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 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++多重继承的复习彻底消除了疑云。