C++知识小屋(3):用虚基类构造仙法·风遁·螺旋手里剑

        类可以说是面向对象的非常重要的思想,在C++这门课程里面,几乎都是围绕类来展开学习的。既然类这么重要,那么跟类相关的知识必然也非常多。所以想在这里把与类相关的知识梳理成一个框架,这样子在面对一个具体问题的时候,就能够知道对应哪部分的知识,应该怎么去使用其中的函数,功能。C++知识小屋🏠系列希望能够对C++的类和对象有一个全面的梳理,同时会给出相关的例子对应知识点,试图用最简单的语言带你一步步掌握晦涩难懂C++的知识。

前情提要

        在学习虚基类的时候,这些困惑就一直围绕着我。
        什么时候该加virtual?
        应该在什么地方加virtual?
        虚基类和其他类的构造函数方法有什么讲究?
        我想通过一个简单易懂的例子来记住它们,于是,就有了这一篇文章。

问题引入

  • 追火影也算是追了十年,必然会时不时会想起里面的经典场景,要数记得最深刻的,应该就是跟佩恩决斗那时候仙人模式下的风遁·螺旋手里剑了。
  • 而如果我们把这个忍术拆分开来,可以发现这一个忍术实际上是好几个忍术的融合:
  1. 普通的螺旋丸
  2. 螺旋手里剑
  3. 风遁·螺旋丸
  4. 风遁·螺旋手里剑
  5. 仙法·螺旋丸
  6. 仙法·风遁·螺旋手里剑
    在这里插入图片描述

类架构

  • 上面提到了该忍术可以拆分成好几个小的忍术,下面是其中一种拆分方式:
    在这里插入图片描述
    螺旋丸含一个数据成员:技能
    风遁螺旋丸:派生自 螺旋丸类增加新成员属性
    螺旋手里剑:派生自 螺旋丸类增加新成员外形
    仙法·螺旋丸:派生自 螺旋丸类增加新成员模式
    风遁·螺旋手里剑:派生自 风遁螺旋丸类螺旋手里剑类无增加新成员
    仙法·风遁·螺旋手里剑:派生自 风遁螺旋手里剑仙法·螺旋丸无增加新成员

重复继承

重复继承:一个派生类2次或2次以上继承同一个基类。

  1. 首先看到上面的类架构图,可以发现存在这么一种现象,比如风遁·螺旋手里剑类继承的两个类中,都间接继承了螺旋丸类,因此螺旋丸类实际上被继承了两次,当我们想访问螺旋丸类的变量时,因为有两条路可以通向螺旋丸类,因此会存在歧义:

    1. 风遁·螺旋手里剑 -> 螺旋手里剑 -> 螺旋丸
    2. 风遁·螺旋手里剑 -> 风遁螺旋丸 -> 螺旋丸
  2. 那么怎么解决这种二义性导致的歧义呢,第一种方法是指明程序是通过螺旋手里剑寻找螺旋丸还是通过风遁螺旋丸寻找螺旋丸,但是这种方法比较麻烦,而且一不小心就容易忘记,而虚基类就很好地解决了这个问题。


虚基类

在从基类派生新的类时,将这个基类用virtual关键字说明为虚基类。
若某个基类被声明为虚基类,则当它被重复继承的时候,派生类创造对象创建一次。若不声明为虚基类,实际上创建了多个基类对象,会存在二义性的问题。

  • 正如虚基类定义所说的那样,构造虚基类可以避免二义性的产生。究其根本,上面出现歧义是因为螺旋丸对象被创建了两次风遁·螺旋手里剑继承螺旋手里剑的时候被构建一次,继承风遁螺旋丸的时候又被构建一次),如果我们只创建一个螺旋丸对象,就不会出现歧义的问题了。这便是虚基类的思想,而我们的螺旋丸类,在这个例子其实就是充当了虚基类这个角色。
  • 简单来说,如果在某个派生关系有向图中,某两个类之间的通路大于1,则需要对作为终点的类(比如上面的螺旋丸类)进行虚基类的构造,否则会出现歧义。

如何定义虚基类

上面都是从理论上讲,下面具体讲虚基类如何定义。为了方便讲解,我们把上面的忍术名字用A,B,C,D表示。

多重继承回顾

如果在定义一个派生类时,该派生类继承了2个或2个以上基类的特征,那么这种继承关系就称为多重继承。

  • 讲虚基类的使用之前,先回顾一下这种情况的继承(多重继承),B继承了A,C也继承了A,但是B跟C是独立的,所以只需要使用普通的公有继承就可以了。
    在这里插入图片描述

多重继承代码实现

#include<iostream>
using namespace std;
//A类
class A{
protected:
	int a;
public:
    A(int a):a(a){cout<<"构建了A类"<<endl;}
};

//B类,公有继承A类
class B:public A{
public:
    B(int a):A(a){cout<<"构建了B类"<<endl;}
};

//C类,公有继承A类
class C:public A{
public:
    C(int a):A(a){cout<<"构建了C类"<<endl;}
};

int main(){
	B test1(1);
	cout << endl;
	C test2(2);
	return 0;
} 

在这里插入图片描述


虚拟继承

与虚基类直接相连的派生类,其继承方式成为虚拟继承(含有“virtual”的继承)

  • 然后就是我们这种情况,因为D->B->A构造了一次A,D->C->A也构造了一次A。而这两次构造A是通过B和C来完成的,因此我们需要声明 B继承A 和 C继承A 的方式为虚拟继承(virtual public)。
  • 那么应该怎么声明B类虚拟继承A类呢?
    在具体的代码实现中,实际上是由上面例子中的"class B:public A"变成“class B:virtual public A”,即在声明继承的时候加上一个“virtual”即可,在B,C类当中,除了这一个区别,其他地方的代码与上面例子的代码完全一致。
    在这里插入图片描述

虚拟继承代码实现(引入虚基类)

#include<iostream>
using namespace std;
//A类
class A{
protected:
	int a;
public:
    A(int a):a(a){cout<<"构建了A类"<<endl;}
};

//B类,虚拟公有继承A类
class B:virtual public A{
public:
    B(int a):A(a){cout<<"构建了B类"<<endl;}
};

//C类,虚拟公有继承A类
class C:virtual public A{
public:
    C(int a):A(a){cout<<"构建了C类"<<endl;}
};

//D类: 公有继承B,C类 
class D:public B,public C{
public:
	D(int a):A(a),B(a),C(a){
		cout<<"构建了D类"<<endl;
	}
}; 

int main(){
	D test(1);	//创建D对象
	return 0;
} 

在这里插入图片描述


构造函数的细节(虚基类只能被直接初始化,间接初始化无效)

        在上面的例子中,我们看到如果我们要声明虚基类,只需要在声明的时候加上‘virtual’即可,这是虚拟继承(存在虚基类的继承关系)和普通继承的第一个区别。
        而虚基类带来的第二个区别是,在虚基类的派生类中,其派生类的构造函数都需要对虚基类进行声明,且只有该声明才对虚基类的对象真正起到初始化作用。

  • 比如下面左边的虚拟继承方法,虚拟继承中的D类,其构造函数必须声明A类,否则编译错误。
  • 而右边的普通继承中,D类的构造函数无需对A进行初始化,因为A的初始化工作交给B类来处理了,跟D没什么关系(普通的间接继承无需考虑爷爷辈分的初始化)
    在这里插入图片描述
            上图解释了“派生类的构造函数都需要对虚基类进行声明”,那么什么叫做“且只有该声明才对虚基类的对象真正起到初始化作用”呢?我们拿上面虚拟继承的代码来说明:
  • 看其中的D类,里面对A,B,C类都用a进行了初始化.
  • 当D类对B类进行初始化时,我们发现B类里面也对虚基类A进行了初始化
  • 当D类对C类进行初始化时,我们发现C类里面也对虚基类A进行了初始化
  • 也就是说看起来A类里面的a被B,C,D类都初始化了一次,但实际上只有D类的初始化才起作用,其他两个初始化形同虚设。

下面我们把上面代码中D类改成下面的代码,其中对A,B,C类的初始化参数分别设置成1,2,3,然后输出真实情况被初始化的值,可发现A类初始化的1才是真正的答案。

//D类: 公有继承B,C类 
class D:public B,public C{
public:
	D(int a):A(1),B(2),C(3){
		cout<<"构建了D类"<<endl;
		cout << endl << "A类中真正的a为: " << a << endl;
	}
};

在这里插入图片描述


一些补充

  • 如果你看完了上面的内容,就能发现虚基类的出现目前带来了两个区别,一个是声明需要加上virtual,第二个是对于虚基类的派生类中,都需要在构造函数中对虚基类进行初始化。
  • 而我刚学这部分内容的时候,曾经好奇过一个点,比如上面例子中,我们需要为B和C两个类进行虚拟继承的声明。那么如果我们只为B声明或者只为C声明行不行呢?如果你不调用虚基类里面的数据成员就可以,否则一定不可以, 会出现下面的报错:
    在这里插入图片描述
    • 总结下来就是,对于上面ABCD四个类的重复继承情况,必须要进行虚基类的声明。
    1. 如果程序中不需要调用虚基类A中的数据成员,则可以只对B或者只对C进行虚拟继承的声明。
    2. 如果程序中需要调用虚基类A中的数据成员,必须对B类和C类对A类的继承都声明为虚拟继承。
    3. 当然最好是养成习惯都写上虚拟继承啦。
  • 可能你还会好奇一点,D类继承B,C类为什么不用声明为虚拟继承呢?其实很简单,因为没有一个类是通向两次D类的,也就是D类不会与其他类构成歧义。当然你把D类的继承声明为虚拟继承对程序也没什么影响。

仙法·风遁·螺旋手里剑代码实现

  • 上面说得有点多,不过也是为了更好地理解虚基类,虚拟继承,多重继承,重复继承等概念。那么我们现在掌握了所有的知识,就可以开始构造仙法·风遁·螺旋手里剑了。我们再看一下该类的架构:
    在这里插入图片描述
  • 显而易见,仙法·风遁·螺旋手里剑->螺旋丸有三条通路,因此与螺旋丸直接相连的三个类都需要声明为虚拟继承。
  • 其他的细节就按照该构造图来构造即可:
    螺旋丸数据成员:技能
    风遁螺旋丸:派生自 螺旋丸类数据成员:技能,属性
    螺旋手里剑:派生自 螺旋丸类数据成员:技能,外形
    仙法·螺旋丸:派生自 螺旋丸类数据成员:技能,模式
    风遁·螺旋手里剑:派生自 风遁螺旋丸类螺旋手里剑类数据成员:技能,外形,模式
    仙法·风遁·螺旋手里剑:派生自 风遁螺旋手里剑仙法·螺旋丸数据成员:技能,属性,外形,模式
  • 建议大家可以亲自复现一次仙法·风遁·螺旋手里剑,这样子对虚基类的了解会更加深刻。
#include<bits/stdc++.h>
using namespace std;

//螺旋丸类 
class Skill{
protected:
    string name;					//忍术名字
public:
	//构造函数
    Skill(string name):name(name){cout<<"螺旋丸类: 			 忍术: "<<name\
    <<endl <<endl;}
};

//风遁螺旋丸类(虚拟继承)  
class Quality_Skill:virtual public Skill{
protected:
    string quality;					//属性名字
public:
	//构造函数
    Quality_Skill(string name,string quality):
	Skill(name),					//构造虚基类对象
	quality(quality)				//赋值属性变量
	{cout<<"风遁螺旋丸类:   		忍术: "<<name<<"   属性为: "<<quality\
	<<endl <<endl;}
};

//螺旋手里剑类(虚拟继承)
class Shape_Skill:virtual public Skill{
protected:
    string shape;					//外形名字
public:
	//构造函数
    Shape_Skill(string name,string shape):
	Skill(name),					//构造虚基类对象
	shape(shape)					//赋值外形变量
	{cout<<"螺旋手里剑 类:  		 忍术: "<<name<<"  外形: "<<shape\
	<<endl <<endl;}
};

//仙法螺旋丸类(虚拟继承)
class Mode_Skill:virtual public Skill{
protected:
    string mode;					//模式
public:
	//构造函数
    Mode_Skill(string name,string mode):
	Skill(name),					//构造虚基类对象
	mode(mode)
	{cout<<"仙法螺旋丸类  			 忍术: "<<name<<"   模式: "<<mode\
	<<endl <<endl;}
};

//风遁·螺旋手里剑类
class Quality_Shape_Skill:public Shape_Skill,public Quality_Skill{
public:
	//构造函数
    Quality_Shape_Skill(string name,string quality,string shape):
	Skill(name),					//构造虚基类对象
	Quality_Skill(name,quality),	//构造风遁螺旋丸对象
	Shape_Skill(name,shape)			//构造螺旋手里剑对象
	{cout<<"风遁·螺旋手里剑类		忍术: "<< name <<"   属性:"<< quality \
	<<"  外形: "<< shape <<endl <<endl;}
};

//仙法·风遁·螺旋手里剑类
class Mode_Quality_Shape_Skill:public Quality_Shape_Skill,public Mode_Skill{
public:
	//构造函数
   Mode_Quality_Shape_Skill(string name,string quality,string shape,string mode):
   Skill(name),								//构造虚基类对象
   Quality_Shape_Skill(name,quality,shape), //构造风遁·螺旋手里剑对象
   Mode_Skill(name,mode)
   {cout<<"仙法·风遁·螺旋手里剑类     	忍术: "<<name<<"  属性:"<<quality \
   <<"   外形: "<< shape << "   模式: "<< mode << endl;}

	string get_name(){
		return name;
	}
};

int main(){
  //构建 仙法·风遁·螺旋手里剑对象test 
  Mode_Quality_Shape_Skill test("螺旋丸","风遁","手里剑","仙法"); 
  
  //输出该类的对象name 
  //cout << test.get_name() << endl;
}



在这里插入图片描述


总结

这节主要梳理了与虚基类相关的知识点,最后再来总结一下:

  • 多重继承:一个派生类继承了2个或2个以上基类的特征。

  • 重复继承:一个派生类2次或2次以上继承同一个基类。

  • 虚拟继承:继承时加入virtual声明的继承方式,为了解决重复继承带来的二义性(歧义)问题。

  • 虚基类:被一个派生类多次继承的类。

    • 继承声明时加上“virtual”
    • 其派生类的构造函数都需要对其进行初始化声明,且只有该声明才真正有效。

       接下来得停更一段时间准备各种ddl了,这篇文章应该是这学期更新的最后一篇,如果大家觉得这种讲述方式不错的话,可以点个赞支持一下喔。🚁

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值