一.多重继承 首先我们先来考虑一个很简单(non-virtual)的多重继承。看看下面这个C++类层次结构。 1 class Top
那么Left、Right、Bottom在内存中如何分布的呢?我们先来看看简单的Left和Right内存分布:
注意到上面类各自的第一个属性都是继承自Top类,这就意味着下面两个赋值语句: 1 Left* left = new Left(); left和top实际上是指向两个相同的地址,我们可以把Left对象当作一个Top对象(同样也可以把Right对象当Top对象来使用)。但是Botom对象呢?GCC是这样处理的:
1 Bottom* bottom = new Bottom(); 这段代码运行正确。这是因为GCC选择的这种内存布局使得我们可以把Bottom对象当作Left对象,它们两者(Left部分)正好相同。但是,如果我们把Bottom对象指针upcast到Right对象呢? 1 Right* right = bottom; 如果我们要使这段代码正常工作的话,我们需要调整指针指向Bottom中相应的部分。
1 Top* top = bottom; 恩,什么结果也没有,这条语句实际上是有歧义(ambiguous)的,编译器会报错: error: `Top' is an ambiguous base of `Bottom'。其实这两种带有歧义的可能性可以用如下语句加以区分: 1 Top* topL = (Left*) bottom; 这两个赋值语句执行之后,topL和left指针将指向同一个地址,同样topR和right也将指向同一个地址。 二.虚拟继承 为了避免上述Top类的多次继承,我们必须虚拟继承类Top。 1 class Top 上述代码将产生如下的类层次图(其实这可能正好是你最开始想要的继承方式)。
1 Bottom* bottom = new Bottom(); 1 movl left, %eax # %eax = left 总结下,我们用left指针去索引(找到)virtual table,然后在virtual table中获取到虚基类的偏移(virtual base offset, vbase),然后在left指针上加上这个偏移量,这样我们就获取到了Bottom类中Top类的开始地址。从上图中,我们可以看到对于Left指针,它的virtual base offset是20,如果我们假设Bottom中每个成员都是4字节大小,那么Left指针加上20字节正好是成员a的地址。 我们同样可以用相同的方式访问Bottom中Right部分。 1 Bottom* bottom = new Bottom(); right指针就会指向在Bottom对象中相应的位置。
当然,关键点在于我们希望能够让访问一个真正单独的Right对象也如同访问一个经过upcasted(到Right对象)的Bottom对象一样。这里我们也在Right对象中引入vptrs。
三.实验 #include <string> //#include <iostream.h> #include <stdio.h> #include <iostream> #include <assert.h> #include <memory.h>
using namespace std; class A { char k[3]; public: virtual void aa(){}; }; class B:public virtual A { char j[3]; public: virtual void bb(){}; }; class C:public virtual B { char i[3]; public: virtual void cc(){}; }; int main() {
cout<<sizeof(A)<<endl; cout<<sizeof(B)<<endl; cout<<sizeof(C)<<endl;
return 0; }
结果:8 20 32 分析: 1.在虚继承时,子类虚函数不是直接加在父类虚函数表下面,而是自己生成虚函数表,也就是不同类生成自己的虚函数表,而不是叠加在父类后面。
2.虚继承内存情况: 虚类指针—子类虚表指针—子类成员变量—父类虚表指针—父类成员变量
总结: 1. 虚继承和非虚继承的区别: (1)非虚继承:子类虚函数加在父类虚函数的下面,总共是需要一个虚表指针。 虚继承:子类虚函数不能加在父类虚函数下面,不同类的虚函数自己建立虚函数表,子类父类都有自己的虚函数表,有两个虚表指针。 (2)非虚继承:成员变量是按显示父类,后是子类的顺序存储的。 虚继承:非虚继承把父类和子类的成员变量放在一起,父类和子类的虚函数放在一起这样存储的。 虚继承是按类存储的,先是子类,后是父类,父类放在最后面。 |