Effective C++ 读书笔记(六)
6 、继承和面向对象设计
条款 32.确定你的public继承朔模出is-a关系
- 以C++面向对象编程,最重要一个规则是:public inheritance(公开继承)意味着“is-a”(是一种)的关系。在这里是“直译”,例如
class D: public B
直译就是D是一种B。 - “public”继承意味着is-a。适用于base classes身上的每一件事一定也适用于derived身上,因为每一个derived对象也是一个base class 对象。
条款 33.避免遮掩继承而来的名称
-
对于变量和函数,子类的名称会遮掩父类的名称,即使是函数重载也不行
class Base{ private: int x; public: virtual void mf1()=0; virtual void mf1(int);//重载 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();//正确,调用Derived::mf1 d.mf1(x);//错误,因为Derived::mf1遮掩了Base::mf1 d.mf2();//正确,调用Base::mf2 d.mf3();//正确,调用Derived::mf3 d.mf3(x);//错误,因为Derived::mf3遮掩了Base::mf3
因为以作用域为基础的“名称遮掩规则”,base class内所有名称为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3都不再被Derived继承。
-
为了避免这个问题,可在子类中使用
using
声明或使用 转交函数(forwarding functions)。如:-
使用using声明;会把所有的都继承过来
class Base{ private: int x; public: virtual void mf1()=0; virtual void mf1(int); virtual void mf2(); void mf3(); void mf3(double); …… }; class Derived: public Base{ public: //让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见,且为public using Base::mf1; using Base::mf3; virtual void mf1(); void mf3(); void mf4(); …… };
则上面的就不会出错了
Derived d; int x; d.mf1();//正确,调用Derived::mf1 d.mf1(x);//调用Base::mf1 d.mf2();//正确,调用Base::mf2 d.mf3();//正确,调用Derived::mf3 d.mf3(x);//调用Base::mf3
-
交换函数
仅仅只想继承Base内mf1无参数的那个版本,则需要用一个简单的交换函数
class Base{ public: virtual void mf1()=0; virtual void mf1(int); }; class Derived: private Base{ public: virtual void mf1()//转交函数(forwarding function) {Base::mf1();};//隐式成为inline }; Derived d; int x; d.mf1();//调用Derived::mf1 d.mf1(x);//错误,Base::mf1被遮掩了
-
条款 34 :区分接口继承和实现继承
- 纯虚函数目的是让子类之继承函数接口,不关心子类怎么去实现
- 非纯虚函数是为了让子类可以选择的实现这个接口,或是父类的默认实现
- 成员函数目的是让子类继承这个函数以及他的实现,而不去改变他(所有子类都使用这个相同的实现)
条款35 : 考虑 virtual 函数以外的其它选择(学习了设计模式之后再来看)
-
使用 non-virtual interface(NVI)手法,这是一种名为 模版方法 模式的设计模式,使用成员函数包裹虚函数。
class GameCharacter{ public: int healthValue() const { …… //做事前工作 int retVal=doHealthValue();//真正做实际工作 …… //做事后工作 return retVal; } …… private: virtual int doHealthValue() const //derived classes可以重新定义 { …… } };
-
将虚函数替换为函数指针的成员变量。Strategy设计模式的应用
class GameCharacter;//forward declaration int defaultHealthCalc(const GameCharacter& gc);//健康计算缺省算法 class GameChaaracter{ public: typedef int(*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc) :healthFunc(hcf) {} int healthValue()const { return healthFunc(*this); } …… private: HealthCalcFunc healthFunc;//函数指针 };
- 同一人物类型之不同实体可以有不同的健康计算函数。也就是说同一人物类型不同的对象可以有不同的健康计算,例如射击游戏中,一些购买防弹衣的玩家使用的对象,血量可以减少更慢。
- 某已知人物健康计算函数可以在运行期间变更。即健康计算函数不再是GameCharacter继承体系内的成员函数。
-
将虚函数替换为
tr1::function
。扫描二维码关注公众号,回复: 6057779 查看本文章 -
将继承体系内的虚函数替换为另一个继承体系内的虚函数(策略模式)。
条款 36:绝不重新定义继承而来的non-virtual 函数
-
因为non-virtual函数是静态绑定的(statically bound,条款 37)。pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本。但是virtual函数是动态绑定(dynamically bound,条款 37),所以virtual函数不受这个约束,即通过指针调用,实际调用的函数是指针真正指向对象的那个函数。
class B{ public: void mf(); …… }; class D: public B {……}; //调用 D x; B* pB=&x; pB->mf();//虽然指向的是派生类对象,但是只能是B所定义的版本,不是多态的行为 D* pD=&x; pD->mf();
-
记住 任何情况下都不该重新定义一个继承而来的 non-virtual 函数。
在条款 7已经知道,base class内的析构函数应该是virtual;如果你违反了条款 7,你也就违反了本条款,析构函数每个class都有,即使自己没有编写
条款 37 :绝不要重新定义继承而来的缺省参数
-
对于以下继承
class Shape{ public: enum ShapeColor{ Red, Green, Blue}; virtual void draw(ShapeColor color=Red) const=0; …… }; class Rectangle: public Shape{ public: virtual void draw(ShapeColor color=Green) const;//不同缺省参数值,很糟糕 …… }; class Circle: public Shape{ public: virtual void draw(ShapeColor color) const; /*客户调用上面函数时,如果使用对象调用,必须指定参数值,因为静态绑定下这个函数不从base继承缺省值。*/ /*如果使用指针或引用调用,可以不指定缺省参数值,动态绑定会从base继承缺省参数值*/ …… };
-
对象的静态类型是他在程序中被声明时产生的类型,动态类型可以表现出一个对象将会有什么行为,动态类型可以在执行过程中改变,重新赋值可以改变动态类型。
Shape* ps; Shape* pc=new Circle;//pc动态类型是Circle* Shape* pr=new Rectangle;//pr的动态类型是Rectangle
这些指针都是静态类型Shape*,动态类型是指目前所指的对象类型
-
virtual函数是动态绑定的,调用哪一份函数实现的代码,取决于调用的那个对象的动态类型。
pc->draw(Shape::Red); pr->draw(Shape::Red);
-
如果不带有参数类型
pr->draw();//调用Rectangle::draw(Shape::Red)
上面调用中,pr动态类型是Rectangle*,所以调用Rectangle的virtual函数。Rectangle::draw函数缺省值是GREEN,但是pr是静态类型Shape*,所以这个调用的缺省参数值来自Shape class,不是Rectangle class。这次调用两个函数各出了一半的力。
-
想要 virtual 函数表现出你想要的行为却有遇到麻烦,聪明的做法就是使用条款 35 里的替代方案。
条款38 :通过复合塑模出has-a或“根据某物实现出”
-
复合(也叫组合 composition)是 has-a 关系,指 “有一个”。我们将某种类型中包含其它类型的关系叫做复合。
class Address{ …… }; class PhoneNumber{ …… }; class Person{ public: …… private: std::string name; Address address; PhoneNumber mobilePhone; };
Person对象中包含string,Address,PhoneNumber对象,这就是复合。还有几个同义词:layering(分层),containment(内含),aggregation(聚合),embedding(内嵌)
-
如果两个类之间并非严格的 is-a 关系(set与list),继承不再适用,我们应该选择复合的形式实现它们。
条款39 :明智而审慎的使用private继承
- private 继承意味着父类所有非私有成员在子类中都是 private 的。这样就帮我们复用父类代码且防止父类接口曝光(只能子类内部使用)。
- 但是私有继承意味着不再是 is-a 关系,而更像是 has-a 关系。我们总是可以通过复合的方式替代私有继承,并且更容易理解,所以无论什么时候,只要可以,我们还是应该选择复合
- 一种极端情况下,即我们有一个空白的父类(不包含non-static变量、virtual函数,没有继承virtual base class,但是含有typedef、enum、static或弄-virtual函数),私有继承可以更小的占用空间。
条款40:明智而审慎的使用多重继承
-
多继承比单继承复杂,而且多继承可能导致二义性(必须明确指出调用类名::),以及对 virtual 继承的需要。
-
多重继承还会导致菱形继承
class File{……}; class InputFile: public File{……}; class OutputFile: public File{……}; class IOFile:public InputFile, public OutputFile {……};
IOFile里面有File的两重拷贝(如果File里面有个成员变量,则IOFile里面有两份,是重复的)
解决方案:
-
缺省方案(就上面所说的执行复制)
-
让带有此数据的class即(File)成为一个虚基类
虽然避免了成员变量重复,但是编译器付出了代价,使用virtual继承的对象比non-virtual继承的对象体积大,访问virtual base classes成员变量是,也比访问non-virtual base classes成员变量速度慢。
class File{……}; class InputFile: virtual public File{……}; class OutputFile:virtual public File{……}; class IOFile:public InputFile, public OutputFile {……};
-
-
父类中存在数据的话,virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,应尽量不使用 virtual 继承。
对虚拟继承的忠告:
- 非必要时,不使用virtual继承,平时使用non-virtual继承。
- 如果必须使用virtual继承,那么尽量避免在base内放置数据,这样就不用担心在这些classes身上的初始化和赋值带来的诡异事情了
-
多继承适用的一种场景是:public 继承某个接口类并 private 继承某个协助实现的类
class IPerson{ public: virtual ~IPerson(); virtual std::string name() const=0; virtual std::string birthDate() const=0; }; class DatabaseID{……}; class PersonInfo{ public: explicit PersonInfo(DatabaseID pid); virtual ~PersonInfo(); virtual const char* theName() const; virtual const char* theBirthdayDate() const; …… private: virtual const char* valueDelimOpen() const; virtual const char* valueDelimClose() const; …… }; class CPerson: public IPerson, private PersonInfo{ public: explicit CPerson(DatabaseID pid): PersonInfo(pid){} virtual std::string name() const { return PersonInfo::theName(); } virtual std::string birthDate() { return PersonInfo::theBirthDate(); } private: const char* valueDelimOpen() const{return "";} const char* valueDelimClose() const{return "";} };