前一篇介绍了C++继承的特征,我们本篇将介绍C++继承的分类
-
单继承:在单一继承中,一个类只能从一个类继承。 即一个子类只能继承一个基类,Java就是这种继承类型。
-
多继承:这是C ++面像对象的特性,一个子类可以从多个父类中继承属性和方法,这正是本篇的主题。它的语法模版如下所示
class 子类名称 :继承模式 父类名1,继承模式 父类名2,.....{
//类代码体
}
为了深入浅出,本篇拿我以前玩过的《最终幻想X-2》为例子,如果你玩过RPG游戏的话,你会知道下面的例子是多么地贴切适合本篇的主题.
但需要说明的是,这是仍然不太好的设计形式。我这里仅仅是用来说明C++多继承的特性。然而它不太符合现实中复杂的需求,我们本文最后会给出更符合复杂需求的方案。
一个角色,可以装配两个职业晶球-黑魔法师和白魔法师,那么角色Role就继承了上面BlackMagic类和WhiteMagic类的所有方法了。
多继承的示例
这是上面UML类图的代码实现
#include <string>
#include <iostream>
using namespace std;
class BlackMagic{
public:
BlackMagic(){cout<<"初始化-黑魔法师"<<endl;}
~BlackMagic(){cout<<"内存回收-黑魔法师"<<endl;}
size_t fira(){
cout<<"fire初级火魔法"<<endl;
}
size_t thurder(){
cout<<"初级雷魔法"<<endl;
}
size_t bio(){
cout<<"毒属性魔法"<<endl;
}
};
class WhiteMagic{
public:
WhiteMagic(){cout<<"初始化-白魔法师"<<endl;}
~WhiteMagic(){cout<<"内存回收-白魔法师"<<endl;}
size_t haste(){
cout<<"haste魔法"<<endl;
}
size_t holy(){
cout<<"圣属性魔法"<<endl;
}
size_t might(){
cout<<"青魔法"<<endl;
}
};
class Role:public WhiteMagic,public BlackMagic{
string d_name;
size_t d_hp;
public:
Role(const string& name,
size_t hp):d_name(name),d_hp(hp){
cout<<"角色初始化 -"<<d_name
<<"HP:"<<d_hp<<endl;
}
~Role(){cout<<"内存回收-游戏角色"<<endl;}
size_t attck(){
cout<<d_name<<"施加attack方法"<<endl;
}
size_t magic(){
cout<<d_name<<"施加magic方法"<<endl;
}
};
int main(void){
Role yuna=Role("Yuna",1500);
yuna.fira();
yuna.holy();
return 0;
}
多继承的RAII约定
首先我们,从下面的输出结果可以看出子类在初始化过程
- 首先按照子类实现的继承列表中的父类顺序执行初始化父类的构造函数
- 然后,初始化子类本身的构造函数。
垃圾回收的过程即会继承列表中定义的父类顺序相反。
- 首先,调用函数在结束之时隐式执行子类的解构函数。
-
然后,依次逆序执行子类继承列表中父类的解构函数。
谨慎使用多继承
如果在实际项目中,你能够想到什么不足之处吗?如果你了解《最终幻想X-2》这个游戏的职业系统设定有些了解的,你体会到上面的代码上下文组织太过死板缺乏灵活性。FFX-2光是职业设定就有10多个,如果一个Role类要继承10个职业设定的其中2个,那么需要编写多少次上面示例代码组合?
你要编写继承组合需要那么多个囧.....
显然多继承无法适用于那些变数类型太大的情况,因此在你使用多继承进行组织代码之前,请在心中考虑这些问题。
- 你设计的类中属性和方法的实现明确并且后期不会变动吗?
- 你设计的类的作为多继承链中的父类,他们公开的接口不会有所变动吗?
- 你在多继承链中混合使用各种继承模式(即public,protected,private等模式),子类中的属性和方法生成的最终的访问控制修饰符,你能够一目了然吗?
如果你自信心十足的话~恭喜你,小弟想拜你为师。
另外C++的多继承另外一个😈邪恶的一面,就是棱形问题,考虑如下这个UML类图存在什么问题?
ss_17.png
当一个类的两个父类具有共同的基类时,就会发生"棱形问题"。 例如,在上图的继承链中,Role类到Ability类,它获取了多少个属性副本?这是一个不合理的设计,根据上面分析的多继承RAII约定中
Role类的分类初始化流程会如下顺序依次执行初始化:
- Ability() ➔Magic() ➔BlackMagic()
- Ability()➔Magic() ➔WhiteMagic()
- Ability()➔Physical()➔Warrior()
因此整个多继承链中Magic的构造函数执行了2次,Ability的构造函数执行了3次,因此Role构造整个过程中,获得了2份Magic实例中的属性副本,3份Ability实例中的属性副本。
这对于计算机资源管理角度考虑这是不可取的。所以对多继承没有深入认识的话,会对你的代码组织会造成各种负面的效果。
我们模拟一下上面分析过程的步骤1和步骤2上面,只需修改上文的示例代码,但注意这个是一个设计不良的示例。
class Ability{
public:
Ability(){cout<<"初始化 - Ability实例"<<endl;}
~Ability(){cout<<"内存释放 -Ability实例"<<endl;}
};
class Magic:public Ability{
public:
Magic(){cout<<"初始化 - Magic实例"<<endl;}
~Magic(){cout<<"内存释放 - Magic实例"<<endl;}
};
class WhiteMagic:public Magic{
....
};
class BlackMagic:public Magic{
....
};
最后输出的程序结果,跟我们以上分析的结果一样
多继承的可选方案
多继承就我个人而言,在局部的上下文中应用还是可以的,如果大规模使用的话,我更倾向于用其他流行的设计模式作为优选方案。而且在打算动手敲代码之前,不妨想本文示例中,用UML类图和工程流程图理清了你的思路之后再动手组织代码。上面的示例代码,我们可以使用常见的组合模式来组织代码,比如N个职业设定,我们都单继承于一个Career的基类,并且我们向Role类的构造函数传递2个Career类,这种组织代码的模式更为灵活,能够适用绝大多数场合。
class Career{...};
class BlackMagic: public Career{...};
....
class WhiteMagic:public Career{...};
....
class Role{
public:
Role(Career ca1,Career ca2,string name,...)
{...}
};
后记
我们最后一篇会提到多继承中,子类如何调用父类指定构造函数的问题
网友评论