Item 32 介绍了public inheritance 公有继承实现"is-a"关系。Item 33-Item 36介绍public inheritance使用中需要注意的地方。其中,Item 33 告诫不论non-virtual,virtual/pure-virtual 都应该避免覆盖;Item 34、35、37 告诫virtual的使用需要注意的地方,35还提供了虚函数virtual替代方案;Item 36 告诫non-virtual 不要当作virtual使用。
Item 38、39 介绍了public inheritance场景以外其他场合该用的情形:复合,实现 ”has-a”、”is-implemented-in-terms-of”关系。Item 39 介绍了private 继承,提供了另一种实现“is-imlemented-in-terms-of”关系的方法。
Item 40 介绍了多重继承存在的问题和适用的场景
(1)概念:在B适用的同样D也适用,D就可以从B 公有继承过来。public inheritance 意味着"is-a"。适用于base classes 的每一件事也适用于derived classes,因为derived class object都是一个 base class object。例如:学生和“人”的关系。
(2)公有继承的双方的一个性质:c++领域中,任何期望引用类型为Person的函数,都可以接受一个Student对象,反之不成立。
(3)作者在类的设计上的观点:“没有一个适用于所有软件的完美设计”、“最好的设计依赖于系统究竟期望做什么,无论现在还是未来”。
这里的原因和继承没有关系,属于c++作用域范畴。该Item告诉我们D(derived class)中的名字会覆盖B(base class)中的名字,这从来不是我们想要的。如下面的例子,D(derived class)进行了覆盖。B(Base class)中的mf1和mf3被D(derived class)中的mf1和mf3覆盖。
这么操作会带来吃惊的意外:
Base class中的所有名为mf1和mf3的函数被Derived class中的mf1和mf3覆盖。但实际上我们是需要继承Base class中的overloads的。
解决办法是:使用using declarition 或者 forward function,这里effective c++作者提供了一个绕过“c++”对“通过继承得到的名字”的缺省覆盖机制:如果base class 对成员函数有重载的话,derived class 需要为每一个不想覆盖的名字使用using declarition, 否则,一些希望继承下来的名字会被覆盖。
注:作者为说明举例 mf3不是个好例子,mf3本身就不该在Derived class里重新定义。mf1更有警示价值。
在考虑基类的设计时,对每个成员函数,我们该声明成哪一类,这样可以精确指定我们需要派生类继承什么东西,而不是统一随意的都声明为non-virtual或者都为virtual类型。
(1.1)声明一个纯虚函数pure virtual的目的是使 类D(derived class)仅仅继承一个函数的接口,适用于派生类对该接口的实现都不同的情形。如果非要给纯虚函数提供定义也可以,调用的时候需要用class name限定修饰这个调用(几乎没人这么用)。
(1.2)声明一个虚函数virtual的目的与纯虚函数不同,其目的是类D(derived class)既继承类B(base class)的接口,也继承一个来自类B(base class)的实现,该实现作为类D的缺省实现存在。
(1.3)声明一个非虚成员函数non-virtual,使类D(derived class)既继承接口,又继承实现,且该实现不会改变。
(2.1)风险点:类D(derived class) 如果忘记改写实现,这时类B(base class)提供的缺省行为又不是我们想要的,那么实际的行为会与预期不符!
(2.2)规避办法:切断虚拟函数的接口和它的缺省实现之间的联系。“为derived class提供缺省行为,但是除非他们提出明确的要求,否则就不交给他们”可以很容易做到,从而达到规避缺省继承带来的问题。具体的实施方案如下:
方法1:说白了就是利用”pure virtual 接口必须实现”的规定,再将缺省实现放到类B(base class)的另一个独立的成员函数中,让这个成员函数非虚non-virtual。如下:
原来的B-D :
class Airport { ... }; // represents airports
class Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{ default code for flying an airplane to the given destination }
class ModelA: public Airplane { ... };
class ModelB: public Airplane { ... };
修改后的B-D:
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0; //fly成为一个纯虚函数,D必需实现
...
protected:
void defaultFly(const Airport& destination); };
void Airplane::defaultFly(const Airport& destination)
{ default code for flying an airplane to the given destination }
class ModelC: public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{ code for flying a ModelC airplane to the given destination }
方法2:前半部分想法同方法1,将接口声明为纯虚函数pure virtual, 只不过不再单独提供non-virtual函数, 改为直接实现pure virtual修饰的接口。例如:
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination) // an implementation of
{ // a pure virtual function default code for flying an airplane to the given destination }
class ModelA: public Airplane
{
public:
virtual void fly(const Airport& destination) { Airplane::fly(destination); }
...
};
class ModelB: public Airplane
{
public:
virtual void fly(const Airport& destination) { Airplane::fly(destination); } ... };
class ModelC: public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination) { code for flying a ModelC airplane to the given destination }
这里作者给我们展示了脱离面向对象设计的习惯性道路,我们还能怎么做。
1、使用non-virtual interface idiom(非虚拟接口惯用法)——让客户通过公有非虚函数调用私有虚拟函数。这种设计是一种被称为Template Method的设计模式的特殊形式。
NVI idom 是公有虚拟函数的比较有趣的可选替代物,本质还是用虚拟函数来计算不同角色的xxx. 公有非虚函数成了一个壳。这种方式的好处也显然易见:可以将原本要在不同派生类重新定义 实现 的相同部分“摘出来”,而把真正的不同的工作交给派生重定义去做。
2、使用策略模式:
1的本质说白了还是在虚拟函数上下功夫,2就完全跳出了虚拟函数的范畴。
如果该虚拟函数被认为可以不依赖于对象的类型。那么我们可以为构造函数传递一个函数指针,该类外函数执行这部分计算。有下3种不同形式的方法:
(2.1)用函数指针数据成员代替虚拟函数。这样做好处:机动性更强,体现在(1)不同示例可以有不同的计算方法,具体用哪个在定义对象的时候决定(2)指定的对象,其计算方法可以在运行时改变。缺点:可能需要以降低封装性为代价(因为在类外,如果该函数执行所需要的所有内容,只通过角色的公有接口就能获取到就没什么问题。如果需要非公有构件就会产生问题,只能用友元破坏封装性为代价)。
(2.2)用 tr1::function 数据成员代替虚拟函数。这样就允许使用兼容于你所需要的东西的任何可调用实体。好处:相对于1而言,更灵活了。用函数对象代替函数指针作数据成员,更灵活。typedef 定义的函数指针,参数与返回类型是呆板的,如果用函数对象,这些约束就会消失。这样的对象可以持有any callable entity(任何可调用实体),如函数指针,函数对象、或者成员函数指针,只要兼容于该函数对象所期待的形式就行。
(2.3)用 virtual function in another hierarchy (另一个继承体系中的虚拟函数),也是一个经典方案,通过把HealthCalcFunc设计成一个类,可以方便别人派生以修改计算方法。
如果要改写,就应该声明为virtual. 这种操作,不论是理论上还是实践上都说不过去。理论上,这体现出设计上考虑不周,与“is-a”的关系矛盾;实际操作上,non-virtual 是静态绑定,不同指针/引用类型导致调用的实际操作不同——两面性,一个B类(base class)引用一个D类(derived class)对象,和一个D类引用一个D类对象,同一个对象不同的引用者其表现不同,产生两面性。
这是因为,虚拟函数是动态绑定的,而缺省参数值是静态绑定的。一个对象有,静态类型和动态类型。静态类型就是,声明给它的type(类型);动态类型,取决于它当前引用/指向的对象的类型。虚拟函数是动态绑定的, 意味着被调用的特定函数取决于调用它的那个对象的动态类型而非静态类型。但是如果重定义了带缺省参数的虚拟函数,就导致最终调用了一个定义在派生类中的虚拟函数,其参数用的却是来自基类的缺省参数值。
注:缺省参数值不动态绑定的原因,是出于运行的效率考虑的。 但是如果我们就是要重定义带缺省参数值的虚函数,怎么办呢?这个时候需要借助Item 35提到的虚拟函数的替代设计方案,比如可以用 NVI idom方案替代,把缺省参数值放在non-virtual接口里,实际的活定义在private的doxxx里干,派生类只需重定义这个doXXX就完事了。
这两种情况public继承不再适用,应该用“复合”的方式设计。
常见的场景:应用领域和实现领域。在应用领域,复合意味着“has-a”;在实现领域,复合意味着“is-implemented-in-terms-of”。
private继承意味着“is-implemented-in-terms-of”(根据某物实现出)。作者告诉我们 私有继承纯粹是一种实现技术,意味着只有实现的部分被继承,接口的部分被忽略。如果D从B私有继承,它就意味着D对象是根据B对象实现的,除此之外没有其他含义。
从含义来看,和item 38的“复合”意义相同,effective c++ 建议我们这种场合,尽量先考虑“复合”来满足,后考虑使用private继承。
使用private继承的场合:
(1)当保护成员或虚拟函数掺杂进来的时候。“is-implemented-in-terms-of”情形,D类需要重新定义 B类的虚函数,那么这种场合,就不得不使用private继承。这块可以参考item 40提供的例子,CPerson和PersonInfo。
(2)当空间相关的极端情况。
基于第一点,作者举例:widget和Timer。
widget要利用这个Timer实现对自己成员函数的统计。如果有个现成的Timer,那么只需改写Timer的虚拟函数onTick就能实现wiget统计的需求。这种改写的需求,使得widget必需从Timer继承,此处公有继承显然不合适,widget不是Timer,Timer的接口不是widget的客户想要的。因此只能私有继承。
然后可以在private中重定义onTick,实现widget期望的行为。
注:使用private继承的场合,我们可以用“复合”的办法实现:例如刚才的例子。可以从Timer公有继承 widgetTimer类,对Timer类的ontick 实现进行改写,并在widget中放置widgetTimer的对象timer,这种方式也能解决我们的widget类想要使用Timer的意图。
容易出现的2个问题:
(1)从一个以上的base classes继承相同名称,可能会导致歧义,即使一个在base1的public部分,另一个在base2的private部分,也不能避免(因为编译器只有在确认最佳匹配后才检查可取用性,如果2者的匹配性一样,那么编译器就认为没有最佳匹配,也就不会继续检查可取用性。)
(2)重复继承。作者指出可以用虚拟继承来解决这个问题,不过代价是对象的占用空间增加、访问数据成员的时间变长、派生类的初始化变得复杂。
多继承的使用场景:如果方案涉及组合 从一个接口类的公有继承 和从一个实现类的私有继承,在SI实在办不到的情况下,考虑MI。UI开发的场景多重继承用的多。