32:确定你的public继承塑模出is-a关系
class Bird
{
public:
virtual void fly();//鸟可以飞
...
};
class Penguin:public Bird//企鹅 是一种鸟类 但是不能飞
{
...
};
解决方法有两种:
1.重新设计继承体系
class Bird
{
public:
...
};
class FlyingBird:public Bird
{
public:
virtual void fly();//鸟可以飞
...
};
class Penguin:public Bird//企鹅 是一种鸟类 但是不能飞
{
...
};
2.为企鹅重新定义fly函数,让它在运行期产生一个运行期错误
class Penguin:public Bird//企鹅 是一种鸟类 但是不能飞
{
public:
void fly(){error("Attempt to make penguin fly!!");}
...
};
条款18规定,好的接口可以防止无效的代码通过编译,因此你应该采取在编译器拒绝企鹅飞行的设计而不是只在运行期才能侦测它们。
请记住
- "public"继承意味is-a。适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象。
33:避免遮掩继承而来的名称
编译器查询某个变量或函数,会先查找局部作用域,然后是当前类作用域,然后是父类作用域…最后是全局作用域。
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int){}
....
};
class Derived:public Base
{
public:
virtual void mf1();
...
};
Derived d;
d.mf1();
int a = 12;//正确 derived::mf1()
d.mf1(a);//错误 derived中的mf1遮掩了base中的mf1
解决方法:使用using声明式达成目标
class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int){}
....
};
class Derived:public Base
{
public:
using Base::mf1;//让基类内名为mf1所有东西在派生类作用域内都可见。
virtual void mf1();
...
};
Derived d;
d.mf1();
int a = 12;//正确 derived::mf1()
d.mf1(a);//正确
请记住
- 派生类内的名称会遮掩基类内的名称。在public继承下从来没有人希望如此
- 为了让被遮掩的名称重见天日,可使用using声明式或转交函数(派生类成员函数中调用基类中被遮掩的同名函数)。
34:区分接口继承和实现继承
1.成员函数的接口总是被继承
2.声明一个pure virtual函数的目的是尉了让派生类只继承函数接口。
3.声明简朴的(非纯)impure virtual的函数的目的,是让派生类继承该函数的接口和缺省实现。
4.声明非虚函数的目的是为了令派生类继承函数的接口和一份强制性实现。
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void AirPlane::fly(const Airport& destination)//纯虚函数实现
{
缺省行为;
}
class ModelA:public Airplane
{
public:
virtual void fly(const Airport& destination)
{
Airplane::fly(destination);
}
...
};
现在fly函数被分割成两部分,声明部分表现的是接口,定义部分表现的是缺省行为。这样的话,可以避免ModleA继承不正确的fly实现代码了,因为基类中纯虚函数要求它必须有自己的fly版本,同时也可以使用基类的缺省实现。
函数类别 | 继承目的 |
---|---|
纯虚函数 | 只继承接口 |
简单虚函数 | 继承接口或一份缺省实现 |
非虚函数 | 继承一份强制实现 |
当你声明你的成员函数时,必须谨慎选择。可以避免经验不足的类设计者常犯的两个错误:
(1)所有函数声明尉非虚函数,这使得派生类没有多余空间进行特化工作。
(2)所有函数声明为虚函数,有些函数不该在派生类中定义。
请记住
- 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口。
- 纯虚函数只具体指定接口继承
- 简朴的非纯虚函数具体指定接口继承及缺省实现继承
- 非虚函数具体指定接口继承以及强制性实现继承
35:考虑virtual函数以外的其他选择
1.Non-Virtual Interface手法实现Template Method模式
- 令客户通过 public non virtual函数调用private virtual函数,成为NVI手法
class GameCharacter
{
public:
int healthValue()const//派生类不能重新定义它 因为条款36
{
...//事前准备工作
int retVal = doHealthValue();
...//事后收尾工作
return retVal;
}
...
private:
virtual int doHealthValue()const//缺省算法,派生类可以重新定义它
{
...
}
};
2.Function Pointers实现Strategy模式
- 同一人物类型不同实体可以有不同的健康计算函数
- 某已知人物之健康指数计算函数可以在运行期变更,例如类可以提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
class GameCharacter;
int defaultHealthCalc(const GameCharacter &gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = DefaultHealthCalc):healthFunc(*hcf){}
int healthValue()const
{return healthFunc(*this);}
private:
HealthCalcFunc healthFunc;
};
class EvilBadGuy:public GameCharacter
{
...
};
int loseHealthQUickly(const GameCharacter &gc);
int loseHealthSlowly(const GameCharacter &gc);
EvilBadGuy ebg1(loseHealthQUickly);
EvilBadGuy ebg1(loseHealthSlowly);
一般而言,需要以非成员函数访问non-public成员函数的办法就是弱化封装,比如友元或为其实现的某一部分提供public访问函数;采用函数指针替换virtual函数,是否足以弥补缺点,是取决于每个不同设计情况而定的。
3.tr::function完成Strategy模式
class GameCharacter;
int defaultHealthCalc(const GameCharacter &gc);
class GameCharacter
{
public:
typedef st::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFucn hcf = defaultHealthCalc):healthFunc(hcf){}
int healthValue()const
{return healthFunc(*this)}
,..
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
{
...
};
EviBadGuy ebg1(calcHealth);//函数
EviBadGuy ebg2(HealthCalculator());//函数对象
GameLevel currentLevel;
EviBadGuy ebg3(std::tr1::bind(&GameLevel::health,currentLevel,_1));//对象的成员函数 _1表示调用GameLevel::health以currentLevel作为GameLevel的对象。
4.古典Strategy模式
传统的Stragegy模式会将健康计算函数做成一个分离的继承体系中的virtual成员函数。
class GameCharacter;
class HealthCalcFunc
{
public:
...
virtual int calc(const GameCharacter &gc)const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc):pHealthCalc(phcf)
{...}
int healthValue()const
{return pHealthCalc->Calc(*this);}
...
private:
HealthCalcFunc *pHealthCalc;
};
摘要:
(1)使用NVI手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数
(2)将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
(3)以tr::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
(4)将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是Stragegy设计模式的传统实现手法。
请记住
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到类外部函数,带来的缺点就是,非成员函数无法访问类的 non-public成员。
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
36:绝不重新定义继承而来的非虚函数
- 绝不重新定义继承而来的非虚函数。
37:绝不重新定义继承而来的缺省参数值
对象的静态类型:它在程序中被声明时所采用的类型。
对象的动态类型:目前所指对象的类型
静态绑定(前期绑定)-函数调用哪一份函数实现代码,根据对象的静态类型确定。
动条绑定(后期绑定)-函数调用哪一份函数实现代码,根据对象的动态类型确定。
Person *pa;//没有所指对象,没有动态类型
Person *pb = new Student;//动态类型是Student
Person *pc = new Teacher;//动态类型是Teacher
class Person
{
public:
virtual void say(string str = "hello")const;
...
};
class Student:public Person
{
public:
void say(string str)const;
...
};
Stuent中say函数:如果通过对象调用,一定要指定参数值,因为静态绑定下,这个函数没有继承Person的缺省值;如果通过指针调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值。
virtual函数是动态绑定,而缺省参数值却是静态绑定。
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-你唯一应该覆写的东西-却是动态绑定。
38:通过复合塑模出has a或”根据某物实现出“
举个例子:
自己编写set来满足空间比速度重要,STL中set通常以平衡查找树实现,使它们在查找,插入,移除元素时时间短,但是每个元素有额外的三个指针空间开销。让set继承list,把一个数值插入到list两次,可以存在两个该数值,如果插入set中,只会存在一个。这个违背了public继承的本质。所以采用复合方式塑模它们,如下:
template<class T>
class Set
{
public:
bool member(const T& item)const;
...
private:
std::list<T> req;
};
//Set成员函数可大量依赖list及STL提供的功能完成
template<typename T>
bool Set<T>::member(const T& item)
{
return std:find(rep.begin(),rep.end(),item)!=rep.end();
}
这个时候的set和list的关系是isimplemented terms of(根据某物实现出)
请记住
- 复合的意义和public继承完全不同。
- 在应用域,复合意味这has a。在实现域,复合意味着is implemented in terms of(根据某物实现出)。
39:明智而审慎地使用private继承
class Person{...};
class Student:private Person{...};
void eat(const Person &p);
Person p;
Student s;
eat(p);//正确
eat(s)//错误
出现以上问题的原因是:class之间的继承关系是private时,编译器不会自动将派生对象转换尉基类对象。
private继承意味这is implemented in terms of(根据某物实现出),但是该如何在复合和private之间抉择呢?尽可能使用复合,必要时才使用private继承(当protected成员和/或virtual函数牵扯进来的时候;激进情况-空间方面的利害关系)。
举个例子:让widget class让它记录每个成员函数的被调用次数,运行期间需要周期性地审查那份信息。
解决方案:继承Timer类,Timer类中有个onTick方法周期性被自动调用一次。所以有如下代码:
class Widget:private Timer//因为public继承会使用户调用到onTick接口,这个是不允许的。所以采用private继承
{
private:
virtual void onTick()const;
...
};
好像还不错,但是你也可以使用复合的方式代替它,如下:
class Widget
{
private:
class WidgetTimer:public Timer
{
public:
virtual void onTick()const;
};
WidgeTimer *timer;
...
};
复合的设计有两个好处:可以使widget有派生类,并可以阻止其重新定义onTick;将widget编译依存性降至最低。如果之前,widget的文件必须有timer的头文件,现在如果将WidgetTimer移到外部,Widget内只包含一个指向WidgetTimer的指针,不需要include和Timer有关的东西。
再举个激进情况的例子:
class Empty{};
class HoldsAnInt
{
private:
int x;
Empty e;
};
上述代码中你会发现:sizeof(HoldsAnInt)>sizeof(int)
,为什么呢?因为大多数编译中,大小为零的独立对象,通常C++官方强制默认安插一个char到空对象内,同时又因为字节对齐的原因,可能使用HoldsAnInt多增加一个int的大小。
class Empty{};
class HoldsAnInt:private Empty
{
private:
int x;
};
改成这样的话:sizeof(HoldsAnInt)==sizeof(int)
这就是所谓的EBO(empty base optimization 空白基类最优化)
大多数的class并非empty,所以EBO很少成为使用private继承的理由。相比较复合比较容易理解,只要可以,还是应该选择复合。
当你面对不是is a关系的两个类,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个虚函数,private继承即有可能成为正当的设计策略。
请记住
- Private继承意味着is implemented in terms of(根据某物实现出)。它通常比复合的级别低。但是当派生类需要访问protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合不同,private继承可以造成empty base最优化。这对致力于”对象尺寸最小化“的程序库开发者而言,可能很重要。
40:明智而审慎地使用多重继承
请记住
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对虚继承的需要(具体原因可以看我的另一篇C++教程博文中的虚基类的内容)。
- 虚继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果虚基类不带任何数据,将是最具使用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class“和”private 继承某个协助实现的class“的两相结合。