继承与面向对象设计
这节解释C++各种不同特性的真正意义,例如“public继承”意味“is-a”,如果尝试让它带有其他意义,就会惹祸上身。virtual函数意味“接口必须被继承”,non-virtual函数意味“接口和实现都必须被继承”。
条款32:确定你的public继承塑模出is-a关系(Make sure public inheritance models “is-a”.)
“public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33:避免遮掩继承而来的名称(Avoid hiding inherited names.)
1)当位于一个derived class成员函数内指涉(refer to)base class内的某物(也许是个成员函数、typedef或成员变量)事,编译器可以找出所指涉的东西,因为derived classes继承了声明于base classes内的所有东西。实际运作方式是,derived class作用域被嵌套在base class作用域内:
假设derived class内的mf4的实现码部分像这样:
编译器查找mf2名称所指涉的东西时,首先查找local作用域(也就是mf4覆盖的作用域),没有找到任何东西名为mf2,再查找其外围作用域,也就是class Derived覆盖的作用域。还是没有找到,于是再往外围移动,本例为base class,在那儿找到了一个名为mf2的东西,于是停止查找。如果Base内还是没有mf2,查找动作便继续下去,首先找内含Base的那个namespace(s)的作用域(如果有的话),最后往global作用域找。
2)C++的名称遮掩规则(name-hiding rules)所做的唯一事情就是:遮掩名称。
上述代码中,base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了,这样的规则使用于base classes和derived classes内的函数有不同的参数类型,而且不论函数是virtual或non-virtual一样适用。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承!
3)如何推翻C++对“继承而来的名称”的缺省遮掩行为,可以使用using声明式达成目标。
4)转交函数(forwarding function)。假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。
5)derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
6)为了让遮掩的名称再见天日,可使用using声明式或转交函数。
条款34:区分接口继承和实现继承(Differentiate between inheritance of interface and inheritance of implementation.)
1)声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
2)声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
3)声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
条款35:考虑virtual函数以外的其他选择(Consider alternatives to virtual functions.)
1)借由Non-virtual interface手法实现Template Method模式
这个流派主张virtual函数应该几乎总是private。保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:
这一基本设计,也就是“令客户通过public non-virtual成员函数调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Methond设计模式(与C++ templates并无关联)的一个独特表现形式。把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。
NVI手法的一个优点隐身在上述代码注释“做一些事前工作”和“做一些事后工作”之中。
NVI手法涉及在derived classes内重新定义private virtual函数。
2)借由Function Pointers实现Strategy模式
这个设计主张“人物健康指数的计算与人物类型无关”,例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
这个做法是常见的Strategy设计模式的简单应用,它提供了某些有趣弹性:
(1)同一人物类型之不同实体可以有不同的健康计算函数。如:
(2)某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
3)借由tr1::function完成Strategy模式
std::tr1::function<int (const GameCharater&)>的目标签名式代表的函数是“接受一个reference指向const GameCharater,并返回int”。这个tr1::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有任何与此签名式兼容的可调用物(callable entity)。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。
“_1”意味“当为ebg2调用GameLevel::health时是以currentLevel作为GameLevel对象”。
若以tr1::function替换函数指针,将因此允许客户在计算人物健康指数时使用任何兼容的可调用物(callable entity)。
4)古典的Strategy模式
传统(典型)的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果如下图所示:
5)将机能从成员函数一到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
条款36:绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function.)
1)non-virtual函数如B::mf和D::mf都是静态绑定(staticallly bound)。这意思是,由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class”的对象。
virtual函数却是动态绑定(dynamically bound),如果mf是个virtual函数,不论是通过pB或pD调用mf,都会导致调用D::mf,因为pB和pD真正指的都是一个类型为D的对象。
条款37:绝不重新定义继承而来的缺省参数值(Never redefine a function’s inherited default parameter value.)
1)virtual函数是动态绑定,而缺省参数却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值。对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。考虑以下的class继承体系:
对象的所谓动态类型(dynamic type)则是指“目前所指对象的类型”。
此例之中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数。Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是Shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class。
2)为什么C++坚持以这种乖张的方式运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值,这比目前实行的“在编译期决定”的机制更慢而且更复杂。
条款38:通过复合塑模出has-a或“根据某物实现出”(Model “has-a” or “is-implemented-in-terms-of” through composition.)
复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。
is-implemented-in-terms-of的一个实例:
Set对象根据一个list对象实现出来。
条款39:明智而审慎地使用private继承(Use private inheritance judiciously.)
1)private继承的首要规则:
(1)如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。
(2)由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。
2)private继承意味implemented-in-terms-of(根据某物实现出)。private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。
3)尽可能使用复合,必要时才使用private继承。
4)EBO(empty base optimization:空白基类最优化)。
在大多数编译器中sizeof(Empty)获得1,在上述情况下几乎可以确定sizeof(HoldsAnInt)==sizeof(int)。
和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
5)当面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。
条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously.)
1)多重继承(multiple inheritance:MI)引起的歧义。
此例之中对checkOut的调用是歧义的,即使两个函数之中只有一个可取用(BorrowaleItem内的checkOut是public,ElectronicGadget内的却是private)。C++解析重载函数调用的规则:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用而言是最佳匹配。
解决歧义的方案:指出想要调用哪一个base class内的函数。
2)virtual继承
(1)使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。
(2)virtual base的初始化责任由继承体系中的最底层(most derived)class负责,这暗示【1】classes若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些basees距离多远;【2】当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。
(3)virtual继承的忠告:【1】非必要不使用virtual bases;【2】如果必须使用virtual base classes,尽可能避免在其中放置数据。
多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。