这几天写的程序应用到多继承。
以前对多继承的概念非常清晰,可是很久没用就有点模糊了。重新研究一下,“刷新”下记忆。
假设我们有下面的代码:
#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++的多继承机制提供了灵活的代码重用,但也需要开发者谨慎处理潜在的二义性和菱形问题。通过学习和实践"**C++多继承Demo**",我们可以更...
《C++多继承的详解教程》是C++之父Bjarne Stroustrup撰写的一篇深入探讨C++多继承机制的论文。在该文中,Stroustrup不仅澄清了多继承在编程语言中的重要性,还驳斥了多继承会使语言复杂化、难以实现和运行成本高的...
### C++继承与派生机制详解 #### 一、引言 C++ 是一种支持面向对象编程的语言,其中继承机制是其重要的特性之一。通过继承,可以创建一个新类(称为派生类或子类),它继承了现有类(称为基类或父类)的所有属性和...
在阅读《C++多继承与虚基类.docx》文档时,你可能会深入了解到虚基类的实现细节、语法以及它在实际项目中的应用案例。理解并掌握这一概念对于编写复杂的C++程序,特别是涉及多继承的场景,是至关重要的。通过文档的...
对于那些担心多继承带来的问题,如菱形继承导致的数据冗余和二义性,如果`CFinalClassMixin`是一个纯类(没有任何数据成员),那么这些问题就不会出现。纯虚基类不会造成额外的数据拷贝,也不会引入二义性问题。 ...
根据给定的文件标题、描述、标签以及部分内容,本文将深入探讨C++中的两个核心概念:继承与组合。这两种机制都是面向对象编程中用于实现代码重用的重要手段,但它们在设计模式、实现细节以及适用场景上存在显著差异...
总的来说,这个C++实现的五子棋项目不仅涵盖了基本的游戏逻辑,还涉及到面向对象设计、类的继承以及算法应用等多个IT领域的知识点,是学习和实践编程技能的好例子。通过这个项目,开发者可以提升在C++中的编程能力,...
在C++编程语言中,多重继承是一种特性,允许一个类(称为派生类或子类)从多个基类(也称父类或超类)继承属性和行为。这种设计模式提高了代码的重用性,使得类可以组合多个基类的功能。在本教程中,我们将深入探讨...
### C++中的继承 在C++编程语言中,继承是一种重要的面向对象编程(OOP)特性,它允许我们创建一个新的类(派生类),该类继承了现有类(基类)的特性和行为。通过继承,我们可以重用代码,提高代码的可维护性和可...
**多态性**是C++05中继承的一个重要特性,尤其是通过虚函数实现的。`virtual`关键字用于声明基类的函数为虚函数,这样在派生类中就可以重写它们,并且在运行时可以动态地决定调用哪个版本的函数。这使得我们可以编写...
C++三种继承方式总结 C++语言中有三种继承方式:public继承、protected继承和private继承。这些继承方式主要是为了改变基类成员的访问属性,使得派生类可以访问基类的成员变量和成员函数。 首先,让我们了解一下类...
公有继承通常用于实现"is-a"关系,保护继承和私有继承则更多地用于实现"has-a"关系或者作为实现细节的封装。选择哪种继承方式取决于设计需求,如需保持基类接口的公共性,应选择公有继承;如果希望限制基类成员的...
### C++继承中的内存布局详解 #### 深入探讨C++对象模型 在C++编程语言中,理解和掌握类的内存布局对于优化程序性能、避免潜在的内存问题至关重要。尤其是在涉及类继承的情况下,深入了解内存如何组织可以帮助...
在C++编程语言中,派生、继承和多态是面向对象编程的三大核心特性,它们为代码的重用和扩展提供了强大的支持。本项目"学生成绩管理系统_已完成"显然是一个利用这些特性构建的实际应用。 1. **派生**:派生是创建新...
在C++编程语言中,类的继承是一种面向对象编程(OOP)的关键特性,它允许我们创建一个新类,该类不仅包含自身的属性和方法,还可以继承另一个已存在的类(称为基类或父类)的属性和方法。这种机制使得代码重用变得...
本篇文章将详细探讨C++中的多重继承及其相关的实现细节。 首先,我们要理解C++中的继承概念。继承是面向对象编程的核心特征之一,它允许创建一个新类(派生类)作为现有类(基类)的扩展。派生类可以访问基类的公有...
在C++中,多态性主要通过虚函数和纯虚函数实现,允许子类覆盖父类的行为,实现动态绑定。这对于构建可扩展和灵活的系统至关重要。 "输入输出"是任何程序的基本需求。C++提供了iostream库来处理输入输出操作,如cin...
本文将围绕《C++继承中的内存布局》这一主题展开,探讨C++中不同类型的继承(如单继承、多重继承和虚继承)在内存布局方面的差异,并分析成员变量和成员函数的访问方式及实现细节。 #### 二、C结构(Struct)的内存...
1. **C++类继承C#基类**:首先,通过托管C++实现一个适配器类,该类同时继承自C#的基类和C++类,以此达到让C++类间接地继承C#类的目的。这一步骤确保了C++类可以访问和使用C#类提供的服务。 2. **动态创建和调用...