继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
public inheritance(公有继承)意味is-a(是一种)的关系。
如果你令class D以public形式继承class B,你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化得概念,而D比B表现出更特殊化的概念。你主张:“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
public继承主张,能够施行于base class对象身上的每件事情,也可以实行于derived class身上。
在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)(只对public继承才成立)。
好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
请记住:
- “public继承”意味is-a。适用于base classes身上的每一件事情一定也使用于derived classes身上,因为每一个derived classes对象也都是一个base classes对象。
条款33:避免遮掩继承而来的名称
C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否是相同或不同的类型,并不重要。即,只要名称相同就覆盖基类相应的成员,不管是类型,参数个数,都无关紧要。派生类的作用域嵌套在基类的作用域内。
C++的继承关系的遮掩名称也并不管成员函数是纯虚函数,非纯虚函数或非虚函数等。只和名称有关。
class Base { public: virtual void mf1()=0; virtual void mf2(); void mf3(); }; class Derived:public Base { public: virtual void mf1(); void mf4(); }; void Derived::mf4() { mf2(); }
名称查找法则:找mf2,时,先查找local作用域(mf4覆盖的作用域),如果没找到及查找外围作用域(class derived覆盖的作用域),还没找到就再向外围移动(base class作用域),如果还没找到,就找内含base class的那个namespace作用域,最后找global作用域。
如果你真的需要用到基类的被名称遮掩的函数,可以使用using声明式,可以使继承而来的,某给定名称的所有同名函数在derived class中都可见。
如果你继承base class并加上重载函数,而且你有希望重新定义或覆写其中一部分,那么你必须为原本可能会被遮盖的每一个名称引入一个using声明式,否则你希望继承的名称会被遮盖。
请记住:
- derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
- 为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。
条款34:区分接口继承和实现继承
函数接口继承(就是声明)和函数实现继承。
- 成员函数的接口总是会被继承。
- 声明一个纯虚函数的目的是为了让派生类只继承函数接口(可以为纯虚函数提供一份定义,调用它的唯一途径是调用它时指出明确的class名称)。
- 声明一个虚函数的目的是让派生类继承该函数的接口和缺省实现。
- 声明一个非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。
请记住:
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
- pure virtual函数只具体制定接口继承。
- 简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
- non-virtual函数具体制定接口继承以及强制性实现继承。
条款35:考虑virtual函数以外的其它选择
令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。
function对象可持有(保存)任何可调用物(也就是函数指针,函数对象或成员函数指针)只要签名式兼容于需求端。
以下几种替代virtual方案
- 使用NVI手法
- virtual函数替换为“函数指针成员变量”
- function成员变量替换virtual函数
- 将继承体系的virtual函数替换为另一个继承体系内的virtual函数
请记住:
- virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
- 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
- tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。
条款36:绝不重新定义继承而来的non-virtual函数
class内声明一个non-virtual函数会为该class建立一个不变性,凌驾其特异性。
请记住:
- 绝对不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
对于non-virtual函数,上一条款说到,“绝不重新定义继承而来的non-virtual函数”,而对于继承一个带有缺省参数值的virtual函数,也是如此。即绝不重新定义继承而来的缺省参数值。因为:virtual函数系动态绑定(dynamically bound),而缺省参数值确实静态绑定(statically bound)。意思是你可能会在“调用一个定义于派生类内的虚函数”的同时,却使用基类为它所指定的缺省参数值。
class Base { public: virtual void mf1(int a=0)=0; }; class Derived:public Base { public: virtual void mf1(int a); /* 这么写如果当客户以对象调用次函数时,一定要指定参数,因为静态绑定下函数并不从base class 继承缺省参数值,但是若以指针或引用调用此函数可以不指定参数值,因为动态绑定下函数会从base class 继承缺省参数值 */ }; Base* b; Base* d=new Derived;//
动态类型:目前所指对象的类型,表现出一个对象将会有什么行为。调用virtual函数,究竟调用那一份代码取决于那个对象的动态类型。
静态类型:变量左边的类型。
class Base { public: virtual void mf1(int a=0)=0; }; class Derived:public Base { public: virtual void mf1(int a=1); }; Base* d=new Derived; d->mf1();//Derived::mf1(int a=0) /* 因为动态类型是Derived,所以调用派生类的mf1,但由于d的静态类型为Base,所以缺省 参数来自于base class而非derived class */
请记住:
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数——你唯一应该覆写的东西——却是动态绑定。
条款38:通过符合塑模出has-a或“根据某物实现出”
复合有两种含义,一、复合意味着has-a(有一个),发生于实现域对象之间;二、is-implemented-in-terms-of(根据某物实现出),发生在应用域对象之间。
请记住:
- 复合(composition)的意义和public继承完全不同。
- 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。
条款39:明智而审慎地使用private继承
class Person {...}; class Student : private Person {...}; void eat(const Person& p); Person p; Student s; eat(p); //ok eat(s); //bad,Student不能被视为Person
如果是private继承,编译器不会将一个derived class对象转换为一个base class对象。
private继承意味着s-implemented-in-terms-of(根据某物实现出),derived class继承base class意味着使用base class内已经准备好的某些属性,而不是因为derived class和base class有任何观念上的联系。private只是技术上的实现,只有实现被继承,接口部分被略去。
还可以使用复合,在class中嵌套一个private class
class Timer { public: explicit Timer(); virtual void onTick() const; }; class Widget:private Timer { public: explicit Widget(); virtual void onTick() const; }; //改为 class Widget { private: class WidgetTimer:public Timer { public: virtual void onTick() const; }; WidgetTimer timer; };
并不存在is-a关系的两个类,一个类需要访问另一个类的protected成员,或需要重新定义其一个或多个virtual函数,private继承可能是一个好的策略。
请记住:
- private继承意味着is-implemented-in-terms-of(根据某物实现出),他通常比符合的级别低,但是当derived class需要访问protected成员,或许呀重新定义继承而来的virtual函数时,此设计是合理的。
- 和复合不同,private可以造成empty class最优化,这对致力于“对象尺寸最小化”的程序开发而言,可能很重要。
条款40:明智而审慎地使用多重继承
class Base { public: void fun(); }; class Base1 Base { private: bool fun() const; }; class Derived:public Base public Base1 { //... }; Derived d; d.fun();//哪个fun,两个函数的匹配性相同,所以base1中的函数也就未被审查,会造成歧义 d.Base::fun()
在使用public继承时,应该为virtual public。但是会带来以下问题:
- class派生自virtual base class而需要初始化,必须认知virtual base class,不论那些base距离有多远。
- 当一个新的derived class加入继承体系中,必须承担virtual base(不论直接还是间接)初始话的责任。
在使用virtual base class时,尽可能的避免放置数据,那么就不必担心这些class初始化和赋值所带来的诡异的事情了。
- 多重继承会发生"成员函数或成员变量重名"、"钻石型继承"等问题,对于这种情况建议使用virtual public继承,但这会带来空间和效率的损失。
- 多重继承的复杂性,导致一般不会使用。只有virtual base classes(也就是继承多个接口)才最有实用价值,它不一定需要virtual public继承。
请记住:
- 多重继承比单一继承复杂,他可能导致新的歧义性,以及对virtual继承的需要。
- virtual继承会带来大小,速度,初始化(赋值)复杂度等等成本,如果virtual base class不带来任何数据,讲师最有使用价值的情况。
- 多重继承的确有正当的途径,其中一个情节涉及“public继承某个interface class”和“private继承某个协助实现的class”的两相结合