《Effective C++》读书笔记V

继承与面向对象设计

这节解释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 tobase 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的那个namespaces)的作用域(如果有的话),最后往global作用域找。

2)C++的名称遮掩规则(name-hiding rules)所做的唯一事情就是:遮掩名称。

 

上述代码中,base class内所有名为mf1mf3的函数都被derived class内的mf1mf3函数遮掩掉了,这样的规则使用于base classesderived classes内的函数有不同的参数类型,而且不论函数是virtualnon-virtual一样适用。从名称查找观点来看,Base::mf1Base::mf3不再被Derived继承!

 

3)如何推翻C++对“继承而来的名称”的缺省遮掩行为,可以使用using声明式达成目标。

 


4)转交函数(forwarding function)。假设Derivedprivate形式继承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。保留healthValuepublic成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:

 

这一基本设计,也就是“令客户通过public non-virtual成员函数调用private virtual函数”,称为non-virtual interfaceNVI)手法。它是所谓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外部函数,带来的一个缺点是,非成员函数无法访问classnon-public成员。

 

条款36:绝不重新定义继承而来的non-virtual函数(Never redefine an inherited non-virtual function.

1)non-virtual函数如B::mfD::mf都是静态绑定(staticallly bound)。这意思是,由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class”的对象。

 

 

virtual函数却是动态绑定(dynamically bound),如果mf是个virtual函数,不论是通过pBpD调用mf,都会导致调用D::mf,因为pBpD真正指的都是一个类型为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*,所以调用的是Rectanglevirtual函数。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中原本是protectedpublic属性。

2)private继承意味implemented-in-terms-of(根据某物实现出)。private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果Dprivate形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。

3)尽可能使用复合,必要时才使用private继承。

4)EBOempty base optimization:空白基类最优化)。

 

 

在大多数编译器中sizeofEmpty)获得1,在上述情况下几乎可以确定sizeofHoldsAnInt==sizeofint)。

和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

5)当面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。

 

条款40:明智而审慎地使用多重继承(Use multiple inheritance judiciously.

1)多重继承(multiple inheritanceMI)引起的歧义。

 

此例之中对checkOut的调用是歧义的,即使两个函数之中只有一个可取用(BorrowaleItem内的checkOutpublicElectronicGadget内的却是private)。C++解析重载函数调用的规则:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用而言是最佳匹配。

解决歧义的方案:指出想要调用哪一个base class内的函数。

 

2)virtual继承

 

(1)使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。

(2)virtual base的初始化责任由继承体系中的最底层(most derivedclass负责,这暗示【1classes若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些basees距离多远;【2】当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

(3)virtual继承的忠告:【1】非必要不使用virtual bases;【2】如果必须使用virtual base classes,尽可能避免在其中放置数据。

多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值