继承与面向对象设计
确定你的public 继承,表达的是is-a 关系
举例
-
鸟
-
鸟会飞
- 大部分鸟会飞
- 一部分鸟,比如,鸵鸟,不会飞
-
分成两类
- 会飞
- 不会飞
- 这样有一个问题,可能有些程序来说,不需要区分,两种会不会飞的鸟,此时,不区分会飞的鸟和不会飞的鸟,不失为一个完美而有效的设计
-
所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞
-
所有的鸟都有fly 的虚函数,但企鹅将其时限为运行时错误
-
企鹅会飞,但尝试那么做的是一种错误
-
企鹅不会飞这一限制,可以由编译器强制实施
-
企鹅尝试飞行,是一种错误,只有运行期才能检测出来
- 也可以通过不给企鹅类定义fly 函数,这样如果有人调用它,就会编译器报错。
-
-
public 继承主张,能够施行于base class 对象身上的每件事情,都可以施行于派生类对象身上
- 代码通过编译并不代表就可以正确运作
避免遮掩继承而来的名称
作用域有关的问题
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
//..
};
class Derived : public Base {
public:
virtual void mf1(); //mf1 有重载的两个类型,我们只定义了一个
void mf3(); // mf3 有重载的两个类型,我们只定义了一个
void mf4();// 新函数
//...
};
void SomeFunc()
{
Derived d;
int x;
d.mf1(); // Derived::mf1
d.mf1(x); // Derived::mf1 掩盖了Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); // 没问题,调用Derived::mf3
d.mf3(x); // 错误,因为Derived::mf3 遮掩了Base::mf3
}
即使,base classes 和 derived classes 内的函数有不同的参数类型,不论函数是virtual 或 non-virtual。这和本条款一开始展示的道理相同,当时函数someFunc 内的double x 掩盖了global 作用域内的int x,如今Derived 内的函数mf3 掩盖了一个名为mf3 但类型不同的base 函数。
这些行为背后的基本原由:防止你的程序或应用框架内建立新的derived class 时附带从疏远的base classes 继承重载函数。通常我们会想继承重载函数。
实际上如果你正使用public 继承而又不继承那些重载函数,就是违反base 和 派生类之间的is-a 关系。我们总是想要重写c++ 对“继承而来的名称”的缺省掩盖行为。
可以使用using 声明式达成目标:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int) { cout << "Base::mf1(int)" << endl; }
virtual void mf2() { cout << "Base::mf2()" << endl; }
void mf3() { cout << "Base::mf3()" << endl; }
void mf3(double) { cout << "Base::mf3(double)" << endl; }
//..
};
class Derived : public Base {
public:
using Base::mf1;
using Base::mf3;
virtual void mf1() { cout << "Derived::mf1()" << endl; }
void mf3() { cout << "Derived::mf3()" << endl; }
void mf4() { cout << "Derived::mf4()" << endl; }
//...
};
void SomeFunc()
{
Derived d;
int x = 0;
d.mf1(); // Derived::mf1
d.mf1(x); // Base::mf1
d.mf2(); // 没问题,调用Base::mf2
d.mf3(); // 没问题,调用Derived::mf3
d.mf3(x); // Base::mf3
}
继承base 类并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么必须为那些原本会被掩盖的每个名称引入一个using 声明式,否则某些你希望继承的名称会被遮掩。
如果不想继承base 类的所有函数:
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int) { cout << "Base::mf1(int)" << endl; }
//..
};
class Derived : private Base {
public:
virtual void mf1() { Base::mf1(); }
//...
};
注意
- 派生类内的名称会掩盖基类内的名称,在public 继承下从来没有人希望如此
- 为了让被掩盖的名称再见天日,可使用using 声明式或转换函数
区分接口继承和实现继承
只继承成员函数的接口
-
纯虚函数
- 可以为纯虚函数提供一份实现代码,但调用它的唯一途径就是“调用时明确指出其class 名称“
- 提供默认,的同时,要求,子类自己选择,如果需要默认,则手动声明
同时继承函数的接口和实现
-
允许重写函数
- 这也是一种,允许默认实现的办法,但,如果既不想每个人都调用默认实现,又需要默认实现,就使用上面的那种,为纯虚函数提供默认实现的方法
-
非纯虚函数
同时继承函数的接口和实现
- 非虚函数
- 不允许重写函数
性能
- 2-8法则, 80% 时间,花在20%代码上,先解决这20%的代码的效率再说吧
考虑virtual 以外的其他选择
class GameCharacter
{
public:
int healthValue()const // 派生类,不重新定义它
{ //
//... // 做一些事前工作
int retVal = doHealthValue(); // 做真正的工作
//... // 做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const;// 派生类可以重新定义它
{
//... // 缺省算法,计算健康指数
}
};
这一基础设计,就是“令客户通过public non-virtual 成员函数间接调用private virtual 函数,称为non-virtual interface 手法。它是所谓Template Method 设计模式的一个独特表现方式。我把这个non-virtual 函数称为virtual 函数的wrapper。
优点:做一些事前工作,做一些事后工作。
通过函数指针,实现Strategy 模式
Strategy 模式
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
策略模式是一种对象行为型模式。
https://design-patterns.readthedocs.io/zh_CN/latest/behavioral_patterns/strategy.html#strategy
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 0; }
class GameCharacter
{
public:
typedef int(*HealthCalcFunc) (const GameCharacter &);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
int healthValue() const
{
return healthFunc(*this);
}
private:
private:
HealthCalcFunc healthFunc;
};
- 同一人物类型之不同实体可以有不同的健康计算函数
- 某已知人物之健康计算函数可在运行期变更。可以提供一个setHealthCalculator ,用来替换当前的健康指数计算函数。
计算其生命值的函数,不再是GameCharacter 继承体系内的成员函数”。如果人物的健康可以纯粹根据该人物public 接口得来的信息加以计算,这没问题,如果需要on-public 信息进行精准计算,有问题。
一般,唯一的能够解决“需要以non-member 函数访问class 的non-public 成分”的方法;弱化class 的封装。例如,class 可声明那个non-member 函数为friends,或是为其实现的某一部分提供public 访问函数。
由tr1::function 完成Strategy 模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc) { return 0; }
class GameCharacter
{
public:
typedef std::tr1::function<int(const GameCharacter &)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}
int healthValue() const
{
return healthFunc(*this);
}
private:
private:
HealthCalcFunc healthFunc;
};
函数对象:
short calcHealth(const GameCharacter&);
struct HealthCalculator {
int operator()(const GameCharacter &)const {
}
};
class GameLevel {
public:
float health(const GameCharacter &)const;
};
class EvilBadGuy :public GameCharacter {
// 同前
};
class EyeCandyCharacter:public GameCharacter{// 另一个人物类型,假设其构造函数与EvilBadGuy 同
//...
};
EvilBadGuy ebg1(calcHealth);// 人物1,使用某个函数计算健康指数
EyeCandyCharacter eccl(HealthCalculator());//人物2,使用某个函数对象
GameLevel currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health,currentLevel,_1));//人物3,使用某个成员函数计算健康指数
古典的Strategy 模式
class GameCharacter;
class HealthCalcFunc {
public:
//...
virtual int calc(const GameCharacter& gc)const
{//...
}
//...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
typedef std::tr1::function<int(const GameCharacter &)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = &defaultHealthCalc):phealthFunc(hcf){}
int healthValue() const
{
return phealthFunc->calc(*this);
}
private:
private:
HealthCalcFunc* phealthFunc;
};
- virtual 函数的替代方案包括NV1 手法及Strategy 设计模式的多种形式。NV1 手法自身是一个特殊形式的Template Method 设计模式
- 将机能从成员函数移到class 外部函数,带来的一个缺点,非成员函数无法访问class 的non-public 成员
- tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
绝不重新定义继承而来的non-virtual 函数
非虚函数,静态绑定。如果调用非虚函数,父类指针指向子类对象时,调用的是父类函数,子类指针调用子类的。
public 继承是,is-a 关系。在class 内声明一个non-virtual 函数会为该class 建立一个不变性,凌驾其特异性。如果你将这两个观点施行于两个classes B 和 D,以及non-virtual 成员函数B::mf 身上,则:
- 适用于B 对象的每一件事,D 适用,每个D 都是一个B
- B 的派生类,继承mf 的接口和实现,因为mf 为非虚函数。
绝不重新定义继承而来的缺省参数值
范围:继承一个带有缺省参数值的virtual 函数
virtual 函数系动态绑定,而缺省参数值却是静态绑定。
对象的所谓静态类型,就是它在程序中被声明时所采用的类型。
class Shape {
public:
enum ShapeColor{Reg,Green,Blue};
virtual void draw(ShapeColor color = Red)const = 0;
};
class Rectangle :public Shape {
public:
virtual void draw(ShapeColor color = Green) const;
};
class Circle:public Shape{};
{
public:
virtual void draw(ShapeColor color)const;
};
如下代码
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
ps,pc,pr 都是pointer-to-Shape 类型,都为静态类型。动态类型:”目前所指对象的类型“。
虚函数系动态绑定而来,调用一个虚函数时,取决于发出调用的那个对象的动态类型。
但,缺省参数是静态绑定,”调用一个定义于派生类内的虚函数“同时,使用基类为它所指定的缺省参数值。很好理解,传参时,是静态的。
通过复合,构建has-a 或”根据某物实现出“关系
某种类型的对象内含它种类型的对象。
比如,人有一个名称,一个地址,一部手机。
根据某物实现出,举例:借助于stl 中的list 实现了自己的SET,此set 底层不适用二叉树。
明智而谨慎地使用private 继承
- 如果类之间的继承关系是private,继承器不会自动将一个派生类对象转换为一个基类对象。这和共有继承的情况不同。
- 由私有继承而来的所有成员,在派生类中都会变成私有属性
私有继承意味着,根据某物实现出。如果让class D 以私有形式继承class B,用意是为了采用class B 内已经备妥的某些特性,不是因为B 对象和D 对象存在有任何观念上的关系。私有继承纯粹是一种实现技术。
尽可能地使用复合,必要时才使用private 继承-----当protected 成员和/或virual 函数牵扯进来地时候。还有,当空间方面的利害关系足以踢翻private 继承的支柱时。
Widget 仅仅需要一个定时操作,但没有需要导出共有的OnClick 接口(即,如果public 继承,它应该可以被当作一个timer 使用)此时,有两种办法:
- 私有继承,
- 共有继承+导入一个新的class。
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;// 定时器每滴答一次,此函数自动被调用一次
};
class Widget :private Timer {
private:
virtual void onTick()const;// 查看Widget 的数据...等等
// 共有继承,将误导外部调用onTick 函数
};
或
class Widget :
class WidgetTimer : public Timer {
public:
virtual void onTick()const;// 查看Widget 的数据...等等
// 共有继承,将误导外部调用onTick 函数
};
WidgetTimer timer;
};
如果Widget 继承Timer,当widget 被编译时Timer 的定义必须可见,所以定义Widget 的那个文件必须#include Timer.h 。但如果WidgetTimer 移除Widget 之外而Widget内含指针指向一个WidgetTimer,Widget 可以只带着一个简单的WidgetTimer 声明式,不再需要#include 任何与Timer 有关的东西。
当你面对”并不存在is-a 关系“的两个classes,其中一个需要访问另一个的protected 成员,或需要重新定义其一或多个virtual 函数,private 继承极其有可能称为正统设计策略。
- private 继承意味着根据某物实现出。它通常比复合的级别低。但是当派生类需要访问被保护的基类的成员,或需要重新定义继承而来的virtual 函数,这么设计是合理的。
- 和复合不同,私有继承可以造成空base 最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。
明智而审慎地使用多重继承
当MI 进入设计景框,程序有可能从一个以上的base 类继承相同名称—>导致较多的歧义---->通过指定基类来消除这种分歧。需要注意:
即使两个函数之中只有一个可取用(一个public,一个private)。c++ 在看到是否有一个函数可取用之前,C++ 首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配后才检验其可取用性。本例的两个checkouts有相同的匹配程度,没有所谓最佳匹配。因此,private 的那个可取用性也就未被编译器审查。
- 钻石型多重继承
两个问题,共同的基类的成员变量经由每一条路径被复制?只有一份副本?
默认,每一条路径被复制。
如果想只有一个副本:virtual base class。
所有,直接继承自这个共有基类的classes采用,virtual 继承即可。
使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes 的成员变量速度慢。
virtual 继承的成本的其他方面:支配”virtual base classes 初始化“的规则比起non-virtual bases 的情况原为复杂且不直观。virtual base 的初始化责任是由继承体系中的最底层负责。
- classes 若派生自virtual bases 而需要初始化,必须认知其virtual bases-不论那些bases 距离多远
- 当一个新的derived class 加入继承体系中,它必须承担其virtual bases 的初始化责任(不论直接或间接)
建议:
3. 非必要,不适用virtual bases
4. 如果必须使用virtual base classes,尽可能避免在其中放置数据。这么,不需要担心这些classes 身上的初始化(和赋值)所带来的问题。
记住
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual 继承的需要
- virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本,如果virtual base clases 不带任何数据,将是最具实用价值的情况
- 多重继承的确有正当用途,其中一个情节涉及”public 继承某个interface class“和”private 继承某个协助实现的class“的两相组合。