C++多继承的细节

来源:互联网 发布:鸦片战争取胜 知乎 编辑:程序博客网 时间:2024/06/10 05:15

这几天写的程序应用到多继承。

以前对多继承的概念非常清晰,可是很久没用就有点模糊了。重新研究一下,“刷新”下记忆。

假设我们有下面的代码:

#include

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++多重继承的复习彻底消除了疑云。 



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1476362


原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小孩吃积食了吐怎么办 11个宝宝不会爬怎么办 5岁宝宝严重挑食怎么办 孩子吃菜口味重怎么办? 孩子挑食不吃菜不吃肉怎么办 微信新的朋友回复频繁怎么办 忘了闺蜜生日怎么办 把闺蜜生日忘了怎么办 忘了闺蜜的生日怎么办 一岁宝宝不吃菜怎么办 幼儿园教案虫子爬进耳朵怎么办 鼻子出血怎么办幼儿说课 英语记不住发音不准怎么办 幼儿园孩子拼音记不住怎么办 孩子总是记不住东西怎么办 走丢了怎么办小班教案 走丢了怎么办小班社会 数学加减个十分不清怎么办 孩子b和d分不清怎么办 小孩b和d分不清怎么办 高一的数学不会怎么办 农村小孩到市里上学怎么办 和外国人打官司输了怎么办 碰见爱说你的领导怎么办 小孩脾气爆一句话就生气怎么办 小孩眼睛哭肿了怎么办 大人吵架吓到宝宝了怎么办 小孩晚上睡觉不踏实怎么办 二宝美籍大宝怎么办 小孩一洗澡就哭怎么办 孩子去外地上学学籍怎么办 非婚生子父亲想要孩子怎么办 非婚生子孩子父亲找不到了怎么办 3岁半宝宝认字怎么办 上课注意力不集中老是发呆怎么办 海绵宝宝吃了会怎么办 宝宝误吞李子核怎么办 24个月宝宝不愿意说话怎么办 孩子凉着肚子吐怎么办 教宝宝说话不会说怎么办 2岁宝宝语言退化怎么办