C++学习---菱形继承问题详解

写在前面:

何为菱形继承?
在这里插入图片描述
B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。

菱形继承存在的问题:

class A
{
public:
	A():a(1){};
	
	void printA(){cout<<a<<endl;}
	
	int a;
};
 
class B : public A
{
};
 
class C : public A
{
};
 
class D:  public B ,  public C
{
};
 
int main()
{
	D d;
	cout<<sizeof(d);
    return 0;
}

输出d的大小为8。也就是d中有2个a成员变量。这样一来如果要使用a就蛋疼报错了。谁知道你要用哪一个?
从上边的例子中,可以看出菱形继承有数据冗余二义性的问题。

那么,如何解决呢?

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。

看下边这个例子:

class Person
{
public :
 string _name ; // 姓名
};


class Student : virtual public Person
{
  protected :
     int _num ; //学号
};


class Teacher : virtual public Person
{
  protected :
     int _id ; // 职工编号
};


class Assistant : public Student, public Teacher
{
  protected :
     string _majorCourse ; // 主修课程
};

void Test ()
{
    Assistant a ;
      a._name = "peter";
}

在这里插入图片描述
可以看到,编译成功。

虚拟继承解决数据冗余和二义性的原理

为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。

class A
{
public:
 int _a;
};

// class B : public A
class B : virtual public A
{
public:
 int _b;
};

// class C : public A
class C : virtual public A
{
public:
 int _c;
};

class D : public B, public C
{
public:
 int _d;
};

int main()
{
   D d;
   d.B::_a = 1;
   d.C::_a = 2;
   d._b = 3;
   d._c = 4;
   d._d = 5;
   return 0;
}

键盘按F10进入调试状态:
在这里插入图片描述
在这里插入图片描述
我们可以看到,后来对于d.C::_a的修改也影响了前一步d.B::_a的值,可见:菱形虚继承后,基类的成员变量只拷贝了一份。

下图是菱形继承的内存对象成员模型:这里可以看到数据冗余。
在这里插入图片描述

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

在这里插入图片描述
下面是上面的Person关系菱形虚拟继承的原理解释:
在这里插入图片描述

菱形继承的总结与反思

菱形继承的数据冗余和二义性的问题,通过虚继承得到解决。

(1)支持到基类的常规转换。也就是说即使基类是虚基类,也照样可以通过基类指针或引用来操纵派生类的对象。

(2)虚继承只是解决了菱形继承中派生类多个基类内存拷贝的问题,并没有解决多重继承的二义性问题。

(3)通常每个类只会初始化自己的直接基类,如果不按虚继承处理,那么在菱形继承中会出现基类被初始两次的情况,在上例中也就是A→B→A→C→D。为了解决这个重复初始化的问题,虚继承对初始化进行了特殊处理。在虚继承中,由最底层派生类的构造函数初始化虚基类。体会一下下面这个例子:
在这里插入图片描述
构造函数和析构函数顺序如下:

C()
E()
A()
B()
D()
F()
~F()
~D()
~B()
~A()
~E()
~C()

可以看出,首先按声明顺序检查直接基类,看是否有虚基类(发现D,E存在虚基类),先初始化虚基类(例中首先初始化C和E)。一旦虚基类构造完毕,就按声明顺序调用非虚基类的构造函数(例中ABDF),析构的调用次序和构造调用次序相反。

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。多继承可以认为是C++的缺陷之一,很多后来的语言都没有多继承,如Java等等

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页