========================
Effective C++ 继承与面向对象设计
书作者:Scott Meyers
原笔记作者:Justin
========================
Item 32 : public 继承意味着 is-a 关系
--------------------------------------------------
tag: public inheritance 公有继承 is-a
每一个类型为 Derived 的对象同时也是一个类型为 Base 的对象,反之不成立。
实际情况中很多“是一个”的体现并不那么纯粹:大师说“鸟”都会飞,但是实际上是有不会飞的“鸟”的。
在公有继承中,有两种办法来解决这种“不纯粹”:
- 多重继承。对于“鸟”的例子,设计一个“鸟”类,然后从中派生出一个“不会飞的鸟”类和一个“会飞的鸟”类,然后再在它们之中分别派生其他具体的“鸟”们。
- 允许运行时出错。还是“鸟”的例子,对于每一个“鸟”类的派生类,不管它是不是能飞,都会有个“飞”的函数。不同的是,能飞的“鸟”就直接飞了,不能飞的“鸟”则会在“飞”函数里说:”对不起,我不能飞,找别人去吧……”(所谓的运行时错误,runtime error)
Item 33 : 避免遮掩继承而来的名称
--------------------------------------------------
tag: scopes
·derived classes 内的名称会遮掩 base classes 内的名称。
·可以使用 Using 声明式或转角函数 (forwarding functions)。
先在本地域中查找(local scope,比如说函数内部)是否有该名字的定义,如果没有找到
往外一层名字域(比如说函数所在的类)中查找,如果没有找到
再往外一层名字域(比如说函数所在类的父类)中查找,如果没有找到
继续忘外一层名字域中查找(比如说函数所在类的父类的父类,等等),一直找到全局名字域(global scope)还是没找到的话,就报告错误。
在“洋葱”的内部某层定义了和外部某层一样名字的函数:使得位于内部的函数“屏蔽”了外部的同名函数(哪怕两个函数拥有不同的参数表)。
第一,在公有继承中,上述的情况是不允许存在的,因为从定义上来说,公有继承中的子类应该具备父类所有的特征和功能,应该“是一个”父类。
第二,如果在上述情况中需要调用/访问被“屏蔽”的函数/对象,有两个方法可以采用:
using。用using“声明”过完整的名字后,就可以“看见”并使用这个函数/对象了。
踢皮球函数(forwarding functions)。编写一个函数,把真正的活踢给别人……
两种方法示例见下,Derived_0是有“屏蔽”问题的类,Derived_1和Derived_2分别是采用了第一种和第二种方法的类。
class Base {
public :
virtual void func_1();
virtual void func_1( int param);
}
class Derived_0: public Base {
public :
virtual void func_1();
}
class Derived_1: public Base {
public :
using Base::func_1;
virtual void func_1();
}
class Derived_2: private Base {
public :
virtual void func_1();
virtual void func_1( int param)
{ Base::func_1(param);}
}
Item 34 : 区分接口继承和实现继承
--------------------------------------------------
tag: function interfaces, function implementations.
·声明一个 pure virtual 函数以让 derived classes 只继承函数接口
·声明 普通virtual 函数以让 derived classes 继承该函数的接口和缺省实现
·声明 non-virtual 函数以令 derived classes 继承函数的接口及一份强制性实现。
class AClass {
public :
virtual void interface_1() = 0 ;
virtual void interface_2()
{ /* the default implementation..*/ }
void interface_3()
{ /* the compulsory implementation..*/ }
// ..
} ;
class AClassDerived {
public :
virtual void interface_1()
{ /* OK you have to implement this..*/ }
virtual void interface_2()
{ /* you can, but don't have to implement this..*/ }
// void interface_3()
// {you can't implement your own..}
} ;
class AClass {
public :
virtual void interface_1. 5 () = 0 ;
protected :
void default_interface_1. 5 ()
{ /* ..*/ }
} ;
class AClassDerived {
public :
virtual void interface_1. 5 ()
{
// you can either do this
default_interface_1. 5 ();
// or implement in your own way..
}
} ;
Item 35 : 考虑 virtual 函数意外的其他选择
--------------------------------------------------
tag: tr1::function tr1::bind (祥书上实例),
Template Method, Strategy ,
·使用 non-virtual interface(NVI)手法,即 Template Method 设计模式的一种特殊形式。以public non-virtual成员函数包裹较低访问性的virtual函数
·将virtual函数替换为“函数指针成员变量”,即Strategy设计模式的一种分解表现形式
·以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物(callable entiry)搭配一个兼容于需求的签名式。也是Strategy设计模式的某种形式。
·将继承体系内的 virtual函数替换为另一个继承体系内的 virtual函数。即Strategy设计模式的传统实现手法。
----------------------------------
一: Non-Virtual Interface 手法实现 Template Method 模式
:让客户通过 public non-virtual 成员函数间接调用 private virtual 函数,此即NVI手法,Template Method 设计模式的一种表现形式。
这个 public non-virtual 函数成为 virtual 函数的外覆器(wrapper)。
优点在于可在 wrapper 中做一些事前工作和事后工作。
NVI 手法中virtual function 不一定得是 private.
----------------------------------
二: Function Pointers 实现 Strategy 模式
:这种方法的实质,就是把接口函数的实现拿到了类之外。类之中只声明接口的形式,只定义一个函数指针。真正干活的函数(实现)都不是类的成员。
这样做带来了一定的灵活性,具体采用哪种实现与类的继承关系是独立无关联的;同时,非类成员函数也有局限性:无法访问类的非公有成员。如果把函数定义为友元或利用公有函数输出私有成员,又会破坏原设计的 封装。如下代码所示:
class AClass
{
public :
typedef void *(Interface)( /* param.. */ );
explicit AClass( Interface pint = defaultInterface) : pInterface(pint)
{}
private :
Interface pInterface;
} ;
在构造AClass对象的时候即可指定Interface的真身,虽然,它无法直接访问AClass的非公有成员。
指针在C++里简单一些,更推崇用对象(如智能指针tr1)来管理接口函数。(是不是想到item13?:))
原理和函数指针是一样的,只不过因为用了对象来管理资源,使得应用更加灵活。当然,要付出更多一点的代码体积和运行时间代价。
class AClass
{
// all are the same with the funtion pointer version
// except for:
typedef std::tr1::function < void ( /* param.. */ ) > Interface;
} ;
---------------------------------
三: 古典策略模式实现,也是我觉得比较漂亮且容易理解的实现方式。
用两个类搞定:
class AInterface
{
public:
virtual void DoInterface(/* param.. */);
};
AInterface defaultInterface;
class AClass
{
public:
explicit AClass(AInterface * pinter = &defaultInterface) : pInter(pinter)
{}
void TryInterface()
{
pInter->DoInterface();
}
private:
pInterface * pInter;
};
Item 36 :绝不重新定义继承而来的non-virtual函数
--------------------------------------------------
tag:
任何情况下都不该重新定义一个继承而来的non-virtual函数,
Item 37 :绝不重新定义继承而来的 缺省参数值
--------------------------------------------------
tag: 静态类型(static type) 动态绑定 静态绑定 前期绑定 后期绑定 bound bind
virtual 函数为动态绑定(dynamically bound),而缺省参数值却是静态绑定(statically bound)。
静态类型 ( static type ):
在程序中被声明时所采用的类型。
Shape* ps; //静态类型为 Shape*, ps没有动态类型,因为为指向任何对象。
Shape* pc = new Circle; //静态类型为 Shape*, pc的动态类型为 Circle*
Shape* pr = new Rectangle; //静态类型为 Shape*, pr的动态类型为 Rectangle*
动态类型 ( dynamic type ) :
目前所指对象的类型,
virtual 函数系动态绑定而来,调用一个virtual函数时,调用哪一份实现代码,取决于发出调用的那个对象的动态类型。
class AClass
{
public :
virtual void func( int param = 123 )
{
// ..
}
} ;
class AClassDerived : public AClass
{
public :
// problematic overwriting the default parameter..
virtual void func( int param = 456 )
{
// ..
}
} ;
int main()
{
AClass * pA = new AClassDerived;
pA -> func();
}
由于函数默认参数的静态绑定特性,pA->func()执行时param事实上被赋予了123,而非子类中期望的456,虽然接下来执行的是子类的函数实现……
C++考虑到执行效率和复杂性方面的代价,规定了只能是静态绑定的。
解决方式:
可以用非虚函数接口(NVI)来解决这个问题,看代码
class AClass
{
public:
void func(int param = 123)
{
funcImpl(param);
}
private:
virtual void funcImpl( int real_param ) = 0;
//..
};
class AClassDerived : public AClass
{
private:
virtual void funcImpl( int real_param )
{
//do whatever you feel like to do here..
}
//..
};
Item 38 :通过 composition 塑模出 has-a 或 is-implemented-in-terms-of
--------------------------------------------------
tag: has-a composition is-implemented-in-terms-of 、根据list实现set(祥书上例)
·复合(composition)的意义同public继承完全不同。
·在应用域(application domain), 复合意味着 has-a,
在实现域(implementation domain),复合意味着 is-implemented-in-terms-of
composition是类型之间的一种关系,即某种类型的对象内含它种类型的对象。
composition、 layering分层、 containment内含、aggregation聚合、 embedding内嵌
composition意味着has-a 或 is-implemented-in-terms-of。
Item 39 :明智而审慎地使用 private 继承
--------------------------------------------------
tag: private inheritance
Private inheritance意味着 implemented-in-terms-of.只有实现部分被继承,接口部分被略去。
若 D 以 private 形式继承 B, 意思是 D 对象根据 B 对象实现而得。
公有继承中的子类对象是可以被转换为它的父类对象的(“是一个”的关系),而私有继承中这种转换是不成立的。
另外一点,私有继承中父类的所有公有和保护成员(public和protected)到了子类中,都变成了私有成员。
因为上面的特性,私有继承并不满足“是一个”模型的需要。更可怜的是,私有继承并不能代表一种设计思路(公有继承代表了“是一个”的模型设计),而仅仅是“有一个”模型的一种实现手段(私有继承过来的所有成员都是私有的,从这个角度来说它就只是“实现”)。
另一种手段大师在Item38中有提过,就是用类成员的方式来构造,名曰composition。
既然两者都能实现“有一个”模型,那么如何选择呢?能用composition就用composition,必需私有继承的时候方才私有继承。
比如我们有个AClass:class AClass{
public:
virtual void Interface_1(/*..*/);
};
以下为私有继承:class BClass : private AClass{
private:
virtual void Interface_1(/*..*/);
//..
};
而下面的composition可以达到一样甚至更好的效果:class AnotherAClass: public AClass{
public:
virtual void Interface_1(/*..*/);
//..
};
class DClass{
private:
AnotherAClass* a;
//..
};
BClass和DClass都实现了“有一个”,但相比之下还是能分辨出长短:
DClass中的AnotherAClass是私有成员,除了它自己没有人能够访问修改;而私有继承的BClass不能保证其“拥有”的AClass实现部分不会被第三者修改,即使是私有继承来的。(为什么这么说?看下去……)
BClass私有继承了AClass,相当于它“有了一个”AClass可以用,可以玩。AClass中的公有/保护成员都变成了BClass的人,但是在享受使用这些成员的同时,BClass还要承担提供这些成员给别人服务的义务。
ITEM35中曾经提到:虚拟函数机制和公有/私有/保护体制是没有任何关系的。因此在例子中的Interface_1有可能在以下的情况中被替代然后“调用”:
一个CClass公有继承了BClass
CClass定义了自己的Interface_1版本
有一个BClass的指针,指向一个CClass的对象,某个操作中调用了Interface_1(CClass的实现版本)
这时候BClass可能要有意见了:它的作者并没有打算让它的继承者修改BClass版本的Interface_1,但事实是CClass违背了它的意志!
很曲折哈?希望我下次读的时候还能看懂@#¥%
DClass由于只是定义了一个指向AnotherAClass的指针,那么在定义DClass的文件中就不需要include AClass或AnotherAClass的头文件。于是就避免了编译依赖(compilation dependecies)
而BClass因为是继承了AClass,在BClass的文件中就需要加上AClass的头文件,也就不可避免的产生了编译时的依赖。
由此看来,绝大部分情况下,组合方式(composition)是要优于私有继承的。之所以说“绝大部分”,是因为大师说了:
对于EBO(Empty Base Optimization)的情况,私有继承就显现出了它的优势。
所谓EBO就是这样的一种情况,有一种特殊的类,它没有非静态数据成员(non-static data member),也没有虚函数(于是不会需要空间存储虚表)。
所以这样的一种类其实不占用任何空间,不过因为C++不允许0字节的对象存在,而且很多编译器都会添加额外的空间来实现字节对齐,于是这种特殊的类的实际大小应该是1个char对象的大小。
在这种类中,往往会有很多typedef,enum,静态数据成员或者是非虚函数。所以他们还是有价值的。
需要在“有一个”关系中利用这种类的时候,如果采用composition,那么根据上面的结论,就需要付出额外的空间来“存放”这个本来不占空间的类。
然而如果是私有继承呢,就可以避免这种情况。
Item 40 :明智而审慎地使用多重继承
--------------------------------------------------
tag: Multiple Inheritance, MI
MI 的优与劣。
MI 的第一个问题就是名字冲突, 最经典的例子就是钻石问题 (diamond problem)。
设想 A 中有一个函数叫做 GetName(), B 和 C 中都将有这一函数成员,这个时候 D::GetName() 的真正实现是来自 B 的还是 C 的呢?二义性出现了 (ambiguity) 。
不过如果真的发生了这种情况,要解决的方法也不是没有,可以这样做:
D d;
d.B::GetName(); //Calling B's implementation
另外一个高阶一点的方法叫做虚继承 (virtual inheritance) 。对于在虚拟继承中的父类,其中的成员都保证不会在后面的子类中出现二义现象 (ambiguity) 。
class A
{
public:
void GetName();
};
class B : virtual public A { };
class C : virtual public A { };
class D : public B, public C { }
D d;
d.GetName(); //there is no ambiguity here.
但是虚继承不是没有代价的,大师说这种技术会使得最终代码变得更大,访问虚拟继承中的父类成员也会变得更慢一些。
这个也不难理解。和空间换时间一样,和不给牛吃草牛就不干活一样。 ( 另外的一个代价我还没能完全理解透彻:书上说因为虚继承中基类的初始化是由继承关系中最底层的子类负责的,因此对于这些最底下的 “ 嫡孙 ” 类来说,就不是那么方便了 )
于是大师建议只有在必要的时候才使用虚继承,而在虚继承中的基类里也不要放置数据成员,这样就不用担心初始化的问题了。
不过存在就是合理,还是有需要用到 MI 的时候。一个在书中提到的使用 MI 的情形是:当需要从一个类 AClass 中继承接口,又需要从另外一个类 BClass 中继承实现细节时,就可以考虑在公有继承 AClass 的同时又私有继承 BClass 。道理大致就是这样,就不编造程序画蛇添足了。
总结一下: MI 比 SI(Single Inheritance) 要复杂容易出错 ( 比如说钻石问题 ) ,即使可以用虚继承来解决钻石问题,但是其带来的代码体积增大,访问效率下降以及初始化问题还是不能忽视的。最后话说回来,需要用到 MI 的时候,小心点用便是
posted on 2010-03-15 22:54
Euan 阅读(401)
评论(0) 编辑 收藏 引用 所属分类:
C/C++