C++多继承的细节

news/2024/7/7 7:07:59

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

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

假设我们有下面的代码:

#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



http://www.niftyadmin.cn/n/3658778.html

相关文章

Qt读写文件的简单封装

C#中&#xff0c;有下列函数可以简单地读写文件&#xff1a;读: temp File.ReadAllText("abc.txt",Encoding.Default); 写: File.WriteAllText("abc.txt", temp, Encoding.Default);追加: File.AppendAllText("abc.txt", temp, Encoding.Def…

web http方法

Post&#xff08;新增&#xff09;,Put&#xff08;修改&#xff09;,Delete&#xff08;删除&#xff09;,Get&#xff08;查询&#xff09; GET&#xff1a;生到数据列表&#xff08;默认&#xff09;&#xff0c;或者得到一条实体数据 POST&#xff1a;添加服务端添加一条…

将项目托管到gitHub

一、下载并安装Git版本控制工具 下载地址&#xff1a;https://git-scm.com/downloads 注册GitHub账号&#xff1a;https://github.com/ 为什么托管到GitHub要下载Git&#xff1f; git是一个版本控制工具   github是一个用git做版本控制的项目托管平台。 二、在IEDA中设置G…

C++中对文件进行读写操作

#include "stdafx.h"#include#include#includeusing namespace std;//从键盘上读取字符的函数void read_save(){ char c[80]; ofstream outfile("f1.dat"); //以输出方工打开文件 if(!outfile){ cerr<<"open error!"<exit(1); }…

降级论:智商高的IT人,你比沙县老板强吗

从前有三个屌丝&#xff0c;聚在一起做网络&#xff0c;提供免费的网络服务&#xff0c;砸锅卖铁&#xff0c;通宵达旦&#xff0c;除了卖肾啥都做了。3年后终于做到了五百万用户&#xff0c;对于年轻人来说&#xff0c;能把五百万人玩弄于鼓掌之间&#xff0c;已经是很牛逼轰轰…

用指向基类对象的指针输出数据

#include "stdafx.h"#include#includeusing namespace std;class student{private: int num; int age; float score;public: student(int ,int ,float); void display();};//定义构造函数 student::student(int n,int a,float sc):num(n),age(a),score(sc){}void stu…

Number of Under-Replicated Blocks问题

跑了一个mapreduce发现集群上出现了7个Under-Replicated Blocks&#xff0c;在web页面上能看到&#xff0c;在主节点上执行&#xff1a; $ bin/hadoop fsck -blocks 删除导致问题的文件之后就好了。 导致这个问题可能有俩个原因 1. 可能我们默认得block副本因子是3&#xf…

Ambari2.7.0离线部署

一、下载安装包 wget http://public-repo-1.hortonworks.com/ambari/centos7/2.x/updates/2.7.0.0/ambari-2.7.0.0-centos7.tar.gz wget http://public-repo-1.hortonworks.com/HDP-UTILS-1.1.0.22/repos/centos7/HDP-UTILS-1.1.0.22-centos7.tar.gz wget http://public-rep…