6. 继承和面向对象设计(Inheritanceand Object-Oriented Design)
条款32:确定你的public 继承塑模出is-a关系(Makesure public inheritance models “is-a”)
“Public 继承”意味着”is-a”关系。有就是说:所有适用于baseclasses身上的每一件事情也一定适用于derivedclasses,因为每个derivedclass对象也是一个baseclass对象。注意书中给出的企鹅是鸟不能飞的例子,正方形继承长方形。
好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
条款33:避免遮掩继承而来的名称(Avoid hiding inherited names)
继承方式 基类属性 |
public |
protected |
private |
公有继承(public) |
public |
protected |
不可见 |
保护继承(protected) |
protected |
protected |
不可见 |
私有继承(private) |
private |
private |
不可见 |
(1)derivedclasses内的名称会遮掩baseclasses内的名称(即使变量的类型不同,或者函数的参数不同),局部变量名会遮掩全局作用域的变量名。即,只要名称相同就覆盖基类相应的成员,不管是类型,参数个数,都无关紧要。派生类的作用域嵌套在基类的作用域内。C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。如下面代码所示:
class Base{
int x;
virtual void mf1(int);
};
class Derived:public Base{
double x;
virtual void mf1();
};
void main()
{
Derived d;
d.x = 10;
d.mf1(10); //错误!因为Derived::mf1遮掩了Base::mf1
}
(2)如果想继续访问基类内的变量(或函数),可以使用using声明式(usingBase:: mf1)或转交函数(如下所示)。
class Derived:private Base{
public:
virtual void mf1() //转交函数(forwardingfunction)
{
Base::mf1();
}
};
看下面一个例子:
classBase{
private:
int x ;
public:
virtualvoid mf1 = 0 ;
virtualvoid mf2() ;
voidmf3() ;
....
}
classDerived : public Base{
public:
virtual void mf1() ;
voidmf4();
...
}
voidDerived::mf4()
{
....
mf2();
....
}
我们看mf4中mf2的查找顺序:先在mf4的作用域中找,没找到mf2,往外走,在Derived作用域找,没找到,往外,在baseclass中找到了,OK,结束,如果还是没找到的话,就再namespace作用域中找,最后到global作用域找
上面一切OK,但是一旦基类有重载成员,而继承类只改写了一个,那么,基类的其他重载函数就不可见了,看下面的例子:
classBase{
private:
int x ;
public:
virtualvoid mf1 = 0 ;
virtualvoid mf1(int) ;
virtualvoid mf2() ;
voidmf3() ;
voidmf3(double) ;
....
}
classDerived : public Base{
public:
virtual void mf1() ;
voidmf3() ;
voidmf4();
...
}
Derivedd ;
intx ;
d.mf1();
d.mf1(x); // error
d.mf2();
d.mf3();
d.mf3(double); // error
好,如何解决上面的问题,两种方案,一是用using,而是用转接函数
方案1:
classDerived : public Base{
public:
using Base::mf1 ;
usingBase::mf3 ;
virtual void mf1() ;
voidmf3() ;
voidmf4();
...
}
这样一改上面就不会错了
方案2:可能using编译器不支持,也可能你不想全部要,只要一部分
classDerived : private Base{
public:
virtual void mf1() //转交函数
{
Base::mf1() ;
}
}
总结:
1、derivedclasses内的名称会遮掩baseclasses内的名称,在public 继承下,从来没有人希望如此
2、为了让被遮掩的base名称能够重见天日,可以用两种方案:使用using或者使用转交函数
条款34: 区分接口继承和实现继承(Differentiatebetween inheritance of interface and inheritance of implementation)
本条款主要讲函数接口继承(也就是声明)和函数实现继承,以及purevirtual 函数、simple(impure)virtual 、non-virtual函数之间的差异。
(1)接口继承和实现继承不同。在public 继承之下,derived classes 总是继承baseclass的接口(这个没理解)。
(2) purevirtual 函数只具体指定接口继承(要求继承者实现该接口);impure virtual 函数具体指定接口继承及缺省实现继承(继承者可自己实现该接口也可使用缺省实现);non-virtual 函数具体指定接口继承以及强制性实现继承(继承者必须使用该接口在父类的实现)。三类函数的形式如下:
class Shape {
public:
virtual void draw( ) const = 0; //purevirtual 函数
virtual void error(const std::string& msg); //impurevirtual 函数
int objectID ( ) const;//non-virtual 函数
};
(3)注意class设计者最常犯的两个错误:第一个错误时将所有函数声明为non-virtual。这使得derivedclasses没有余裕空间进行特化工作。注意80-20法则,平均而言你的函数调用中可以有80%是virtual而不冲击程序的大体效率。所以应该放在举足轻重的20%代码上头。另一个常见错误时将所有成员函数声明为virtual。某些函数就是不该在derivedclass中被重新定义,果真如此你应该将那些函数声明为non-virtual。如果你的不变性(invariant)凌驾特异性(specialization),别害怕说出来。
条款35: 考虑virtual函数以外的其他选择(Consideralternatives to virtual functions)
本条款告诉程序员,当需要使用virtual函数时,可以考虑其他选择。Virtual函数的替代方案主要有:
(1)使用non-virtualinterface(NVI)手法。思想是:将virtual函数放在private(或protected)中,以publicnon-virtual函数调用该virtual函数。优点是:可以做一些预处理、后处理工作。
class GameCharacter {
public:
int healthValue() const{ // 1. 子类不能重新定义它
... // 2.preprocess
int retVal = doHealthValue(); // 2. 真正的工作放到虚函数中
... // 2.postprocess
return retVal;
}
private:
virtual int doHealthValue() const { // 3. 子类可重新定义它,但子类并不调用它
...
}
};
(2)将virtual函数替换为“函数指针成员变量”(这是Strategy设计模式中的一种表现形式),见下面代码。优点是每个对象拥有自己的函数实现,也可在运行时改变计算函数;缺点是:可能得降低类的封装性,否则非成员函数不能访问类中的non-public成员。
class GameCharacter;
intdefaultHealthCalc(const GameCharacter& gc); // defaultalgorithm
class GameCharacter {
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf =defaultHealthCalc)
: healthFunc(hcf)
{ }
int healthValue() const {
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
(3)以tr1::function成员变量替换virtual函数,这允许使用任何可调用物搭配一个兼容于需求的签名式(可以是函数、函数对象或成员函数)。这也是Strategy设计模式的某种形式。
(4)继承体系内的virtual函数替换为另一个继承体系内的virtual函数。如下UML图所示:
每个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。这是Strategy设计模式的传统实现手法。优点是:可以随时添加新的算法。代码如下:
class GameCharacter;
class HealthCalcFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFuncdefaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc *phcf = &defaultHealthCalc)
: pHealthCalc(phcf)
{ }
int healthValue() const {
return pHealthCalc->calc(*this);
}
...
private:
HealthCalcFunc *pHealthCalc;
};
条款36: 绝对不要重新定义继承而来的non-virtual函数
绝不要重新定义继承而来的non-virtual函数,因为这是一种自相矛盾的设计。Non-virtual函数是用于同时指定函数接口和函数实现的,既然你想重新定义函数(也就是只继承函数接口),就应该定义为virtual的。
此外,本条款还涉及动态绑定(dynamicallybound)和静态绑定(staticallybound),举例如下:
//类的定义
class B{
public:
void func(){ cout<<“B”;}
virtual void func2(){ cout<<“B”;}
};
class D:public B{
public:
void func() { cout<<“D”;}
virtual void func2(){ cout<<“D”;}
};
//下面是对B和D的使用
DdObject;
B* bPtr = &dObject;
D* dPtr = &dObject;
//下面这两种调用方式:
bPtr->func(); //调用B::func
dPtr->func(); //调用D::func
bPtr->func2(); //调用D::func
dPtr->func2(); //调用D::func
解释:在C++继承中,virtual函数是动态绑定的,调用的函数跟指针或者引用实际绑定的对象有关,而non-virtual函数是静态绑定的,调用的函数只跟声明的指针或者引用的类型相关。
条款37: 绝对不要重新定义继承而来的缺省参数值(Neverredefine a function’s inherited default parameter value)
本条款限定在“继承一个带有缺省函数的virtual函数”,给出的建议是:绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。所以,如果你重定义继承而来的virtual函数的缺省参数值,那么,你是用基类的缺省参数值来调用继承类的函数。
如果想重新定义一个继承而来的缺省参数值,可以考虑条款35中virtual函数的替代方案,如NVI技术:令baseclass的publicnon-virtual函数调用privatevirtual函数,后者可被derivedclass重新定义。可以让non-virtual函数指定缺省参数,而privatevirtual函数负责真正的工作。代码如下:
class Shape
{
public:
enum ShapeColor{RED,GREEN,BLUE};
void draw(ShapeColor color = RED) const
{
doDraw(color); //调用virtual函数
}
private:
virtual void doDraw(ShapeColor color )const = 0; //真正的工作,基类类需要重写
};
class Rectangle::public Shape
{
public:
....
private:
virtual void doDraw(ShapeColor color )const; //不需指定缺省参数值
...
};
牢记的概念:绝不重新定义一个继承而来的缺省参数值,因为缺省参数是静态绑定的,而virtual函数----你唯一应该覆写的东西,是动态绑定的。为什么缺省参数是静态绑定而不是动态绑定呢?主要原因是运行效率。如果是动态绑定,程序员用起来很方便,但会降低运行效率,C++为了取舍,结果就是现在这样。
条款38:通过复合塑模出has-a或“根据某物实现出”(Model“has-a” or “is-implemented-in-terms-of” through composition)
复合的意义和public继承完全不同。复合(composition)表示某种类型对象内含其他类型对象。在应用域,复合意味着“has-a”,在实现域,复合意味is-implemented-in-terms-of。
举了两个例子:
1、人有地址啊,电话号码啊,这个叫has-a
2、可以用一个list来实现一个set,这个叫根据某物实现
其实,没什么,我觉得2就像是“适配器模式”
条款39: 明智而审慎地使用private继承(Useprivate inheritance judiciously)
本条款主要介绍private继承的特点和使用场合。
(1)Private继承意味is-implemented-in-terms-of(根据某物实现出)。特点是:如果class之间的继承关系是private,编译器不会自动将一个derived class对象转化为一个base class对象;由privatebase class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原来是protected或public属性。
private继承表示根据某物实现。private继承意味着实现部分被继承,接口部分应略去(因为private继承完后都是private的,外界根本看不到)。如果D以private继承B,意思是D对象根据B对象实现而得,再没有其他含义了。
(2)private继承意味is-implemented-in-terms-of(根据某物实现),这个和条款38说的复合一致。大师指出:尽量用复合,只有当protected成员和/或virtual函数牵扯进来的时候,还有就是空间方面的利害关系。
主要有三种使用场合:两个“并不存在is-a关系”的classes,其中一个需要访问另一个的protected成员;或需要重新定义其一个或多个virtual函数。另一种情况是,需要对emptyclasses的空间最优化,如下面的代码:
class Empty{ }; //empty class
class HoldsAnyInt{
private:
int x;
Empty e;
};//sizeof(HoldsAnyInt)> sizeof(int)。Empty空对象需要安插一个char到空对象,并且有齐位需求。
class HoldsAnyInt::private Empty{
private:
int x;
}; //sizeof(HoldsAnyInt)== sizeof(int),这个就是EBO(emptybased optimization)。
//实际应用中,类Empty中可以放入typedefs,enums,static成员变量,或non-virtual函数。STL中有很多例子。
条款40:明智而审慎地使用多重继承(Usemultiple inheritance judiciously)
本条款主要介绍多重继承的特点和使用场合。
(1)多重继承比单一继承复杂,可能导致新的歧义性(同名时,不知道访问哪个基类的成员),以及对virtual继承的需要(任何派生类中的virtual基类总是用一个共享对象表示)。但是,Virtual继承会增加大小、速度、初始化复杂度等等成本。如果virtualbase classed不带任何数据,Virtual继承将是最具使用价值的情况。
(2)多重继承的一个适用场合:“public继承某个Interfaceclass”和“private继承某个协助实现的class”的两两组合。
考虑下面的情况:
classA {
}
classB : public A {
}
classC : public A {
}
classD : public B , public D{
}
第一个问题:
加入B,C中都有一个mf()函数,那么
Dd ;
d.mf(); //调用的是哪个?歧义
应该这么用:d.B::mf(); // B的mf
第二个问题:
A中的一份数据data,B也有了,C也有了,那么D中有几份?
缺省做法是2份,但可以通过virtual继承防止上面的事情发生
classB : virtual public A {
}
classC : virtual public A {
}
那么是不是所有的继承都应该用virtualpublic呢?
不是,如果你用virtualpublic ,你必然要是靠编译器在背后默默支撑你,因此,代码的体积将增大,速度将变慢。并且,virtualbase class的初始化规则也复杂的多。
大师给了两个观点:
1、非必要不要用virtualbase
2、如果非要用,尽量不要在里面放置数据,这样不用担心初始化的事情。(和JAVA的interface差不多)