C++:菱形继承 (多继承的缺陷)

🍁多继承

在继承语法之后,C++之父本贾尼·斯特劳斯特卢普为了更好的去描绘事物的多样性,引出了多继承的概念:

  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

好比说一个事物有两面性或者说担任多个角色,例如:

圣女果 :既可以是水果又可以是蔬菜
研究生:既可以是老师又可以是学生
间谍:对于敌国是特务坏人,对于本国的是军官好人
在这里插入图片描述

  • 实现多继承的方法即是在派生类的继承子类语法处,以逗号分隔再添不同加子类

举例:

class A
{
protected:
	size_t _a;
};

class B
{
protected:
	size_t _b;
};

class C : public A, public B //派生类C继承多个子类
{
protected:
	size_t _c;
};

注意:派生类在调用构造函数时,默认调用基类的构造函数时的顺序,不是按照派生类构造函数的初始化列表进行的调用,而是看派生类在继承基类时排名先后顺序

好比上面例子:class C : public A, public B
C的构造函数在调用基类构造函数时,先是调用A,再是调用B。

🍁菱形继承

多继承的构想往往是好的一面,但是也让C++多继承语法带来很大的不便,甚至一度人让使用者抓耳挠腮。

为什么这样说呢?我们往下看。

上面提到的研究生为例子,研究生即是担任学生又是老师角色,但最终都是离不开人这个设定。

若是此时学生类和老师类同时继承同一个Person这个基类,那么就会造成以下这样的情况:
在这里插入图片描述

如上情况就犹如闭环的菱形,也就是所谓的菱形继承 (多继承的一种情况):

class Person
{
public :
	string _name;
};

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

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

class Assistant : public Student, public Teacher //继承多个子类
{
protected :
	string _majorCourse ; // 主修课程
};

🍂 菱形继承造成的数据冗余

如上举例的,研究生即可以是担任老师,也可以是担任学生;

假如小明是一个研究生,在学生身份可以被称呼为小明,当教师身份时可以被称为明老师。这样逻辑倒是符合现实场景,似乎看上去很不错,但是事实如此吗?

int main()
{
	Assistant a;
	
	a.Student::_name = "小明";  //指定Student作用域给予赋值小明
	a.Teacher::_name = "明老师"; //指定Teacher作用域给予赋值明老师

	return 0;
}

在这里插入图片描述

上面代码看上去还行,但是我们要称呼一个人的时候应该有一个统称名字:

int main()
{
	Assistant a;
	
	a._name = "张明"; //赋值不明确,报错
	
	return 0;
}

在这里插入图片描述

Assistant 派生类中继承了Student和Person这两个基类,Student和Person这两个类同时又继承了Person这个基类;
此时创建的a对象无法对_name这个成员变量单独进行赋值,只能指定作用域进行赋值。

在这里插入图片描述

就算指定作用域可以解决赋值问题,又会陷入另一个问题:如何去访问_name这个成员变量?又是指定作用域吗?

这个问题是有多麻烦,很矛盾没有,一个统称名字,只能在指定的职位叫指定的名字。

这样的菱形继承生成的两份_name成员造成二义性,变得冗余。这样的情况更是违背了当初继承语法的宗旨:减少代码冗余

🍁菱形虚拟继承

为了解决菱形继承带来的成员变量的二义性以及数据冗余,C++大佬也是绞尽脑汁,想出了菱形虚拟继承的方法:

  • 使用的关键字virtual
  • 在两个派生类要对同一个基类继承时,用关键字virtual修饰进行修饰。虚拟继承可以解决菱形继承的二义性和数据冗余的问题,但是虚拟继承不要在其他地方去使用
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 ; // 主修课程
};

int main()
{
	Assistant a ;
	a._name = "张明";  //解决二义性、数据冗余
	
	return 0;
}

为了方便去辨识,这里进行菱形继承和菱形虚拟继承的对比,先是菱形继承。采用简单的例子,创建四个简单类(A、B、C、D),分别进行以下继承方式:

class A
{
public:
	size_t _a;
};

class B :  public A
{
public:
	size_t _b;
};

class C :  public A
{
public:
	size_t _c;
};

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

int main()
{
	D d;

	d.B::_a = 1; 
	d._b = 2;

	d.C::_a = 3;
	d._c = 4;

	d._d = 5;

	return 0;
}

上述代码为典型的菱形继承,这里在VS2022编译器环境下进行演示。

监视窗口:
在这里插入图片描述
内存窗口:
在这里插入图片描述

从内存窗口里面可以看到菱形继承造成的数据冗余,有两份成员变量_a。如若让对象d直接去调用变量_a的话会产生二义性,编译会直接报错,无法识别。

将代码更改为菱形虚拟继承后:

class A
{
public:
	size_t _a;
};

class B : virtual  public A
{
public:
	size_t _b;
};

class C : virtual public A
{
public:
	size_t _c;
};

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

用原来的方式进行指定作用域赋值(当然也可以不用指定作用域,因为是菱形虚拟继承,这里是为了作对比),先是指定B的作用域对变量_a进行赋值

int main()
{
	D d;
	d.B::_a = 1; //利用d对象指定B作用域对变量_a进行赋值
	
	return 0;
}

监视窗口:
在这里插入图片描述

然后再指定指定C的作用域对变量_a进行赋值:

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 3;  //利用d对象指定C作用域对变量_a进行赋值
	
	return 0;
}

监视窗口:
在这里插入图片描述

结合两次监视窗口可以看到,随着指定作用域给予赋值的时候,两者的变量_a值都同时变化且变化的都是一样的内容。

监视窗口体现的只是冰山一角,从内存窗口可以看到更多的信息体现的更加详细。接着往下看:

🍂菱形虚拟继承的原理

先将各个类的值都进行初始化(由于是菱形虚拟继承没有二义性可以直接进行赋值):

class A
{
public:
	size_t _a;
};

class B : virtual  public A //虚拟继承
{
public:
	size_t _b;
};

class C : virtual public A //虚拟继承
{
public:
	size_t _c;
};

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

int main()
{
	D d;
	//对于变量_a,在菱形虚拟继承后,继承数据只有一份,可以直接进行赋值
	d._a = 1;
	d._b = 2;
	d._c = 3;
	d._d = 4;

	return 0;
}

🍃对比

左边内存窗口展示的是菱形继承例子;右边内存窗口展示的是菱形虚拟继承的例子;
在这里插入图片描述

对比菱形继承和菱形虚拟继承两者窗口,有明显差别:

  1. 菱形继承窗口中:A类成员变量有两份内容,且都是在B类和C类中
  2. 菱形虚拟继承窗口中:D对象中将A类放到的了对象组成的最下面,且仅有一份;对与被D继承的B类和C类来说,成员变量存储的值的位置没有变,但是原本在菱形继承中存A类的地方,在这里变成存储地址

菱形继承和菱形虚拟继承两者差别看完了,还是会懵懵懂懂的不禁让人疑惑:B类和C类中存储地址有什么作用?这个A类同时属于B类和C类,那么B类和C类如何去找到公共的A类呢?

上面提到的地址如果有细心的观察的话,发现B类和C类的地址并不是乱的而是有一定规律

在VS2022编译器在调试下再分别打开两个内存窗口,将这两个地址输入:

在这里插入图片描述
在B和C中存储的地址位置其实是一个指针,名字为虚基表指针。这个两个指针都分别指向了一个虚基表,从上面的虚基表中可以看到存储了两份信息第一份存储的信息都为零,在这里先不用管,因为这个是为了多态做准备的,在这里先不作解释多态篇会解释;第二份信息存储的是地址偏移量

地址偏移量可以帮助B类或是C类找到公共的A类,由于存储的是十六进制,要先换算成十进制才能进行计算:

  • 从B类的虚基表指针位置往下数到A,偏移量的值20
  • 从C类的虚基表指针位置往下数到A,偏移量的值为12

由此就可以解决B类和C类如何去找到公共的A类的问题了。

🍂缺陷与不足

菱形虚拟继承可以很好解决菱形继承带来的成员变量的二义性和数据冗余,但是在实际开发过程中是不推荐使用菱形继承的。如果涉及了多继承那么可能会造成菱形继承,有了菱形继承就会去使用菱形虚拟继承,底层关系就会变得很复杂。

例如上面的子类D的构造函数该如何设计?

class A
{
public:

	A(size_t a)
		:_a(a) 
	{ cout << "A()" << endl; }

	size_t _a;
};

class B : virtual public A
{
public:

	B(size_t a, size_t b)
		: A(a), _b(b)
	{
		cout << "B()" << endl;
	}

	size_t _b;
};

class C : virtual public A
{
public:

	C(size_t a, size_t c)
		:A(a), _c(c)
	{
		cout << "C()" << endl;
	}

	size_t _c;
};

class D : public B, public C 
{
public:
	//D类构造函数
	D(size_t a, size_t b, size_t c, size_t d)
		:_d(d), B(a, b), C(a, c), A(a)
	{
		cout << "D()" << endl;
	}

	size_t _d;
};

上面代码中D类的内容都是比较简单的,如果类的内容十分复杂的话那么弄成菱形虚拟继承的所花费精力和代价是非常大的。

多继承带来的缺陷也是十分明显,其概念也就C++语法中有提及,因此对于大多数的oo语言(面向对象的语言)是没有多继承这个概念的。例如:java

菱形继承带来的问题,以及解决办法就讲到这里。感谢大家支持!!!

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值