Effective C++ 3nd 读书摘要(六、继承与面向对象设计)Item32 - 40

六、继承与面向对象设计
Item32. 确定你的public继承塑模出is-a关系
即Liskov Substitution Principle
下面是一个不严谨的public继承:

Penguin并不会飞。
所以一个较好的替代方案是:

好的设计应该让违反这种关系的继承在编译期间就被检测出来。
public继承主张:能够施行于base class对象身上的_每件事情_都可以施行于derived class对象身上

 

Item33. 避免遮掩继承而来的名称
C++会遮掩外层的函数名称,而不论它们的参数类型相同与否,也不管这些函数是否是虚函数。(public继承

一定不要让其发生!)


使用using声明可以解决这个问题:

但是using声明式会使某一名称的函数在derived class中全部可见(如上例中,using Base::mf3会使其两个版本都可见)。这在public继承中是正确的(因为它是is-a关系),但在private继承中,可能会只想继承其中某一个版本(如那个无参数版本)。则需要下面的转交技术:

 

Item34. 区分接口继承和实现继承
在设计上可以让一个要被继承的纯虚函数(如fly)同时体现出两个基本要素:
声明部分表现的是接口(那是derived classes必须使用的),其定义部分则表现出缺省行为(那是derived classes可能使用的,但只有在它们明确提出申请时才是):

小结pure virtual函数只具体指定接口继承,impure virtual函数具体指定接口继承及缺省实现继承(这样的函数没有不变性,随derived classes而变),non-virtual函数具体指定接口继承以及强制性实现继承(这样的函数没有特化的空间,只能是base class中的那个)。

 

Item35. 考虑virtual函数以外的其他选择
1.籍由Non-Virtual Interface(NVI)实现Template Method模式
源自一个流派,他们主张virtual函数应该几乎总是private。

即-令客户通过public non-virtual成员函数间接调用private virtual。
NVI的一个优点是do "before" stuffdo "after" stuff。这确保得以在一个virtual函数被调用之前设计好适当场景(locking a mutex、log extry、verifying that class invariants and function preconditions are satisfied, etc.),并在调用结束之后清理场景(unlocking a mutex、verifying function postconditions、reverifying class invariants, etc.)。

 

2.籍由Function Pointers实现Strategy模式

这样便得同一人物类型之不同实体可以有不同的健康计算函数。例如:

而且可以在运行期变更计算函数。但是其缺点是当这个传入的non-member函数需要访问class的non-public成分时,我们只能弱化class的封装(如设置为friend、提供public访问函数)。

 

3.籍由tr1::function完成Strategy模式

签名式std::tr1::function<int (const GameCharacter&)>的含义是:接受一个reference指向const

GameCharacter,并返回int。tr1::function产生的对象可以持有任何与此签名式兼容的可调用物。

 

tr1::function比function pointer拥有更大的弹性,下面是三种情况:

①函数
short calcHealth(const GameCharacter&);           // health calculation function;
              //note non-int return type
②函数对象
struct HealthCalculator {                                    // class for health
  int operator()(const GameCharacter&) const     // calculation function
  { ... }                                 // objects
};
③成员函数
class GameLevel {
public:
  float health(const GameCharacter&) const;      // health calculation
  ...                                          // mem function; note
};                                               // non-int return type
定义一些使用者
class EvilBadGuy: public GameCharacter {         // as before
  ...
};
class EyeCandyCharacter:   public GameCharacter {  // another character
  ...                                              // type; assume same
};                                                 // constructor as EvilBadGuy
三种使用方法
EvilBadGuy ebg1(calcHealth);                       // character using a
                                                       // health calculation function
EyeCandyCharacter ecc1(HealthCalculator());        // character using a
                                                  // health calculation function object
GameLevel currentLevel;
...
EvilBadGuy ebg2(                                            // character using a
  std::tr1::bind(&GameLevel::health,               // health calculation
          currentLevel,                                         // member function;
          _1)                                                          // see below for details
);
(将currentLevel绑定为GameLevel对象,_1表示当为ebg2调用GameLevel::health时系以currentLevel作为GameLevel对象)

4.古典的Strategy模式

 

Item36. 绝不重新定义继承而来的non-virtual函数
如果A继承自B,又override了它的non-virtual函数,则此函数被调用时,可能表现出B或A的行为,这得看指

向该对象之指针的声明类型,因为non-virtual函数是statically bound。

 

Item37. 绝不重新定义继承而来的缺省参数值
因为缺省参数值都是静态绑定,而virtual函数却是动态绑定。违反了此条款,就导致调用一个定义于derived

class内的virtual函数的同时,却使用base class为它所指定的缺省参数值。


这时可以考虑替代设计,如NVI:

 

Item38. 通过composition塑模出has-a或is-implemented-in-terms-of
比如你想通过在底层使用list来构造一个set:
template<typename T>                       // the wrong way to use list for Set
class Set: public std::list<T> { ... };

但这种public继承违反了is-a的含义,list中可以有重复元素,但set不可以有,因此set不是一个list。

正确做法是set object can be implemented in terms of a list object:

 

Item39. 明智而审慎地使用private继承
由private继承而来的所有成员,在derived class中都会变成private属性。因此它们都成了实现的枝节。private继承意味着implemented-in-terms-of,这纯粹只是一种实现技术,在软件“设计”层面上没有意义,只在软件实现层面上有意义。

 

如果让class D以private继承class B,含义是为了采用B中已经备妥的某些特性,而不是因为B和D存在有任何观念上的关系。

 

Item38指出composition也用于实现implemented-in-terms-of,但要尽可能使用复合,必要时才使用private继承。

 

例如一个private继承:

也可以换成composition的设计:

composition的设计有两个优点:
1. 使得Widget可以拥有derived classes,但同时又可以阻止derived classes重新定义onTick。这一点private继承无法做到。(此法可用于模拟Java中的final或C#中的sealed)


2. 可以将Widget的编译依存性降至最低。即使WidgetTimer移出Widget体外,也只需要一个class

WidgetTimer声明式,而不用#include WidgetTimer,这是private继承所无法避免的。

 

Private继承有一个优点:可以实现EBOempty base optimization)。
比较以下,先是composition:

将会发现sizeof(HoldsAnInt) > sizeof(int);
而在private中:

几乎可以肯定:sizeof(HoldsAnInt) == sizeof(int)

另外,当derived class需要访问protected base class的成员,或者需要重新定义继承而来的virtual函数时,使用private设计是合理的。

 

Item40. 明智而审慎地使用多重继承
为了避免钻石型继承,应该对直接继承自顶层的classes采用virtual继承:

class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };

 

(thy:virtual继承专为多重继承引入,如果不是virtual继承的话那么如下:

则d.f(); 调用的是B::f(); 如果C,B不是virtual public A的话那d.f()就必须指出f()是类C的还是类D的,d.B::f()或d.C::f())

 

但是virtual继承会比non-virtual继承体积大,访问成员速度慢。另外virtual base的初始化责任是由继承体系中的最低层(most derived)class负责,这意味着若classes派生自virtual bases而需要初始化,它必须认知virtual bases——不论距离多远。

所以 1.非必要不使用virtual bases,2.如果一定要用,尽可能避免在其中放置数据

 

此外,在设计时可以将public继承与private继承加以结合:
class CPerson: public IPerson, private PersonInfo {}
使用“public继承接口”和“private继承实现”。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值