继承与面向对象设计中,涉及了继承方式,即public、private及protected继承方式,继承体系中的屏蔽问题,成员函数virtual、non-virtual的选择以及多重继承等。
[b]Item 32: 确定你的public继承塑模出is-a关系[/b]
以C++进行面向对象编程,最重要的一个规则是:public inheritance意味is-a的关系,于是,基类对象B可以派上用场的地方,继承类对象D一样可以派上用场。
类之间除了存在is-a关系外,还可能存在has-a和is-implementated-in-terms-of的关系。
[b]Item 33: 避免遮掩继承而来的名称
在继承体系下,C++名字查找规则如下:
(1)在函数内查找;
(2)在类内查找;
(3)在基类内查找;
(4)在命名空间内查找;
(5)在全局域内查找。
把(1)~(5)看成在不同域内查找,在每个域内,编译器首先查找所有名字匹配的变量、函数,如果是函数,还会找出其最佳匹配,只要名字在某一步中的领域内匹配了,编译器便不会再往更大的范围查找。编译器总是在匹配完后才检验其可用性,即共访问修饰符public, private等。
我们来看下面一个例子:
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1( int ) = 0;
virtual void mf2();
void mf3();
void mf3( double );
...
};
class Derived : public Base{
public:
virtual void mf1();
void mf3();
void mf4();
...
};
下面是调用的一些情况:
Derived d;
int x;
...
d.mf1(); //OK,调用Derived::mf1
d.mf1(x); //error, Derived::mf1()遮掩了Base::mf1
d.mf2(); //OK, 调用Base::mf2
d.mf3(); //OK, 调用Derived::mf3
d.mf3(x); //error, Derived::mf3遮掩了Base:mf3
如果希望继承base class并加上重载函数,而又希望重新定义或覆写其中一部分,那么必须为那些原本会被遮掩的每个名称引入一个using声明式。如下:
class Derived : public Base{
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
...
};
有时候你并不想继承base class的所有函数,这是可以理解的。但是在public继承下,这绝对不可能发生。因为它违反了public继承所暗示的is-a关系。然而在private继承下它却可能是有意义的。假如Derived以private形式继承Base,而Derived唯一想继承的是那个无参数的mf1版本,可以用forwarding function(转交函数)来实现:
class Derived : private Base{
public:
virtual void mf1(){
Base::mf1(); } //函数转换,暗自成为inline函数
void mf3();
void mf4();
...
};
函数转交的另外一个用途是为那些不支持using声明式的老旧编译器另辟一条新路。
[b]Item 34: 区别接口继承和实现继承[/b]
(1)Derived类对Base类的继承有几种方式:public、private和protected,它们只是改变其他类(包括派生类的派生类)对派生类中基类成员的访问方式。public派生类继承了基类的藏品,它具有与基类相同的接口,这种方式称为接口继承;private和protected成员相当于继承了基类的操作,但并不把它们开放给其他类用户,这种派生通常称为实现继承。
(2)声明一个pure virtual函数的目的是为了让Derived class只继承函数接口。不过,比较意外的是,我们也可以为pure virtual函数提供定义,不过调用它时需要明确指出其class名称。
(3)声明impure virtual函数的目的是让derived class继承该函数的接口和缺省的实现。
(4)声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。
[b]Item 35: 考虑virtual函数以外的其他选择[/b]
virtual函数被用来使同一继承体系的不同对象在运行时选择适合的动作,有时候它是那么显而易见的设计方式,以至于我们忘了考虑其他的可能。这一节提供了两种设计模式,来代替可能本来显而易见的virtual函数。
(1)Non-virtual-interface手法实现Template Method模式
模板方法:在一个方法中定义一个算法的骨架,而这些步骤延迟到子类中去。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
NVI手法用public non-virtual成员函数包裹较低访问性的virtual函数。
(2)Strategy模式
一种方法是将virtual函数替换为“函数指针成员变量”,这种方法使得不同的对象实例(而不是对象)可以拥有不同的处理方法;
比较函数成员指针使用范围更广的是tr1::function,该类型产生的对象可以持有任何与其签名式兼容的可调用物。所谓兼容,是指这个可调用物的参数可被隐式转换为tr1::function指定的参数。可调用物包括函数指针,函数对象以及成员函数指针等。
古典的Strategy模式将继承体系内的virtual函数替换为另一继承体系内的virtual函数。
[b]Item 36: 绝不重新定义继承而来的non-virtual函数[/b]
重新定义继承而来的non-virtual函数,会使我们前面提到的,public继承下,D是B的命题不再为真。
[b]Item 37: 绝不重新定义继承而来的缺省参数值[/b]
重新定义继承而来的non-virtual函数是错误的。那么,而重新定义virtual函数是很自然的。但是,不要重新定义其缺省的参数值,因为:virtual函数是在运行期间动态绑定的,而缺省的参数值却是静态绑定的。也就是,如果基类的virtual函数有缺省参数,在以后不同派生类的该virtual函数都使用基类的缺省参数。
C++这么做的原因是在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种方法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。
[b]Item 38: 通过复合塑模出has-a或is-implemented-in-terms-of[/b]
当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域则是表现is-implemented-in-terms-of。
下面分别是has-a跟is-implemented-in-terms-of的一个例子:
class Address { ... };
class PhoneNumber { ... };
class Person{
public:
...
private:
std::string name; //合成成份物,has-a的关系
Address addr;
PhoneNumber voiceNumber;
};
template
class Set{
public:
bool member( const T& item ) const;
void insert( const T& item );
void remove( const T& item );
std::size_t size() const;
private:
std::list rep; //implement Set in terms of list
};
[b]Item 39: 明智而审慎地使用private继承[/b]
我们知道,当使用public继承时,派生类D和基类B存在is-a的关系,在需要B类的场合中,编译器会自动将D类转换为B类。但是,这只是发生在public继承下,如果是private继承,将不会有这样的转换。
这是private继承的规则:(1)如上所述,编译器不会自动将一个D对象转换为一个B对象;(2)由private base class继承而来的所有成员,在derived class中都会变成private属性。
private继承意味着implemented-in-terms-of。如果你让D以private形式继承B,你的用意是为了采用B内已经备妥的某些特性,不是因为B对象和D对象存在有什么任何观念上的关系。private继承纯粹是一种实现技术。
而且,在需要implemented-in-terms-of时,尽可能使用复合,必要时才使用private继承。何时才是必要的?主要是当protected成员和/或virtual函数被牵进来的时候。
例如我们有一个Widget,它需要某种计时器,在一个周期到的时候执行某个动作,有这样一个计时器:
class Timer{
public:
explicit Timer( int tickFrequency );
virtual void onTick() const;
...
};
可以以private形式继承Timer:
class Widget: private Timer{
private:
virtual void onTick() const;
...
};
但是下面的复合方式似乎更好:
class Widget{
private:
class WidgetTimer : public Timer{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
为什么复合的方法比private继承要好呢?
首先,你可能希望让Widget有派生类,但是又不希望这个派生类重新定义onTick(),如果Widget继承自Timer,这样的想法就没办法实现。
其次,使用复合的方法使得我们可以使用pointer to implementation的方法来降低耦合度。
当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分,或为了重新定义一个或多个virtual函数时,我们可能会选择private继承。
另有一种激进情况可能让我们选择使用private,那只适用于所处理的class不带任何数据。这样的class没有non-static成员变量,没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr),也没有virtual base class(因为这样的base class也会招致何种上的额外开销)。于是这种empty class对象不使用任何空间,因为没有任何隶属对象的数据需要存储。但是C++裁定:凡是独立(非附属)对象都必须有非零大小,所以看下面的例子:
class Empty{ ... }; //没有数据,所以其对象不使用任何内存
class HoldsAnInt{
private:
int x;
Empty e;
};
你会发现,sizeof( HoldsAnInt ) > sizeof( int )。因为Empty作为独立对象,必须有非零大小。
如果你这样实现HoldsAnInt:
class HoldsAnInt : private Empty{ //Empty是附属对象
private:
int x;
};
现在, sizeof( HoldsAnInt ) == sizeof( int );这就是所谓的Empty Base Optimization,EBO。
明智则审慎地使用private继承的意思是,在考虑过所有其他方案之后(如混合了public继承和复合的设计)仍然认为private继承是“表现程序内两个class之间的关系”的最佳办法,这才用它。
[b]Item 40: 明智而审慎地使用多重继承[/b]
多重继承的意思是继承一个以上的base class,但这些class并不常在继承体系中又有更高级的base class。
有多重继承下,程序有可能从一个以上的base class继承相同名称,那会导致较多的歧义,为了解决这个歧义,你必须明白地指出你要调用哪个base class内的函数。
所谓的“钻石型多重继承”,是像下面这样的一种情况:
class File {
public:
...
private:
std::string fileName;
...
};
class InputFile : public File{ ... };
class OutputFile : public File{ ... };
class IOFile : public InputFile, public OutputFile{ ... };
继承自File的InputFile和OutputFile将各自拥有一个成员变量fileName,那么IOFile内该有多少笔这个名称的数据呢?从某个角度说,IOFile从其每一个base class继承一份,所以其对象内应该有两份fileName成员变量;但是逻辑告诉我们,IOFile对象只该有一个fileName数据,所以它继承自两个base class而来的fileName不该重复。
默认情况下,IOFile将同时从InputFile和OutputFile继承fileName成员,也就是拥有两份fileName,除非显式声明virtual public继承。如下:
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
这样看来我们似乎应该总是用virtual public取代public,可是想一想为避免继承得来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承的那些class所产生的对象往往比使用non-virtual继承的兄弟们大,访问virtual base class的成员变量时也比访问non-virtual base class的成员变量速度慢。
因此作者给出的关于virtual继承的忠告:(1)非必要不使用virtual base;(2)如果必须使用virtual base classes,尽可能避免在其中放置数据。
书中最后还用了一个例子来说明多重继承的正当用途,其中涉及“public继承某个interface class”和“private继承某个协助实现的class”的两相组合。
呵呵,感觉这一章总结得很潦草,其中很多想法,大概要到真正应用中,才能体会其艺术吧。