zoukankan      html  css  js  c++  java
  • 【转】C++多继承的细节

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

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

    假设我们有下面的代码: 

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

  • 相关阅读:
    Linux下文件的基本操作
    conpot_usage简要说明
    const声明常量以及特点
    let变量声明以及声明特性
    盒子模型
    文本样式
    行间距
    字体的其他样式
    字体分类
    字体样式
  • 原文地址:https://www.cnblogs.com/jiayy/p/4460223.html
Copyright © 2011-2022 走看看