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

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

假设我们有下面的代码:

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

【转】C++多继承的细节的更多相关文章

  1. Java 方法重载,方法重写(覆盖),继承等细节注意

    1.方法重载(method overload)的具体规范 如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载. 一.方法名一定要相同. 二.方法的参数表必须不同,包括参数的 ...

  2. Java编程的逻辑 (16) - 继承的细节

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  3. ES6 class的继承使用细节

    ES6 class的继承与java的继承大同小异,如果学过java的话应该很容易理解,都是通过extends关键字继承. class Animal{ constructor(color){ this. ...

  4. Maven之 聚合与继承 详解

    说到聚合与继承我们都很熟悉,maven同样也具备这样的设计原则,下面我们来看一下Maven的pom如何进行聚合与继承的配置实现. 一.为什么要聚合? 随着技术的飞速发展和各类用户对软件的要求越来越高, ...

  5. Maven详解之聚合与继承

    说到聚合与继承我们都很熟悉,maven同样也具备这样的设计原则,下面我们来看一下Maven的pom如何进行聚合与继承的配置实现. 一.为什么要聚合? 随着技术的飞速发展和各类用户对软件的要求越来越高, ...

  6. 黑马程序员——OC语言 三大特性之继承

    Java培训.Android培训.iOS培训..Net培训.期待与您交流! (以下内容是对黑马苹果入学视频的个人知识点总结) 三大特性之继承 (一)继承的基本用法 先建立个Animal再用Dog继承前 ...

  7. maven之详解继承与聚合

    说到聚合与继承我们都很熟悉,maven同样也具备这样的设计原则,下面我们来看一下Maven的pom如何进行聚合与继承的配置实现. 一.为什么要聚合? 随着技术的飞速发展和各类用户对软件的要求越来越高, ...

  8. Maven详解 之 聚合与继承

    说到聚合与继承我们都很熟悉,maven同样也具备这样的设计原则,下面我们来看一下Maven的pom如何进行聚合与继承的配置实现. 一.为什么要聚合? 随着技术的飞速发展和各类用户对软件的要求越来越高, ...

  9. Maven聚合和继承的详细解释

    说到聚合与继承我们都非常熟悉,maven相同也具备这种设计原则.以下我们来看一下Maven的pom怎样进行聚合与继承的配置实现. 一.为什么要聚合? 随着技术的飞速发展和各类用户对软件的要求越来越高. ...

随机推荐

  1. .Net MVC 实现长轮询

    什么是长轮询? 长轮询是“服务器推”技术实现方式的一种,可以将服务端发生的变化实时传送到客户端而无须客户端频繁的地刷新.发送请求. 长轮询原理? 客户端向服务器发送Ajax请求,服务器接收到请求后,保 ...

  2. html的body内标签之多行文本及下拉框

    一,<textarea>默认值<textarea>  -name属性,textarea的默认值放到中间 <select> name,内部option value,提 ...

  3. 【刷题】BZOJ 1003 [ZJOI2006]物流运输

    Description 物流公司要把一批货物从码头A运到码头B.由于货物量比较大,需要n天才能运完.货物运输过程中一般要转停好几个码头.物流公司通常会设计一条固定的运输路线,以便对整个运输过程实施严格 ...

  4. [洛谷P3979]遥远的国度

    题目大意:有一棵$n$个点的树,每个点有一个点权,有三种操作: $1\;x:$把根变成$x$ $2\;u\;v\;x:$把路径$u->v$上的点权改为$x$ $3\;x:$询问以$x$为根的子树 ...

  5. [NOIP2010] 引水入城 贪心 + 记忆化搜索

    ---题面--- 题解: 本蒟蒻并没有想到bfs的做法,,,, 只会dfs了 首先我们需要知道一个性质. 我们设k[i].l 为在i点建立水库可以支援到的最左边的城市,k[i].r为最右边的. 那么点 ...

  6. BZOJ3631:[JLOI2014]松鼠的新家——题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=3631 https://www.luogu.org/problemnew/show/P3258 松鼠的 ...

  7. HDOJ(HDU).1059 Dividing(DP 多重背包+二进制优化)

    HDOJ(HDU).1059 Dividing(DP 多重背包+二进制优化) 题意分析 给出一系列的石头的数量,然后问石头能否被平分成为价值相等的2份.首先可以确定的是如果石头的价值总和为奇数的话,那 ...

  8. bzoj2330: [SCOI2011]糖果(差分约束)

    差分约束裸题,a==b的话分别建a>=b a<=b的边就行.倒序加边不然会TLE是什么鬼 #include<iostream> #include<cstring> ...

  9. 添加网站标题logo

    如何在标题栏title前添加网站logo? 第一种方法:据说在网站根目录下放着我们的ico型logo,命名为favicon.ico,浏览器会自动去找到并显示.试了试,在firefox23和ie8下都没 ...

  10. OLTP与OLAP比较【转】

    OLTP与OLAP的介绍 数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing).联机分析处理OLAP(On-Line Analytical ...