Effective C++读书笔记(6)——继承与面向对象设计

确定你的public继承塑模出is-a关系

public继承意味着is-a。适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每个derived class对象也都是一个base calss对象。

这章不知怎么写,感觉要自己体会吧。

避免遮掩继承来的名称

这个其实是跟作用域关系比较大,就是derived class内的名称会遮挡base classes内的名称。在public的继承下从来没有人希望如此。

class Base{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
	virtual void mf2();
	void mf3();
	void mf3(double);
};

//derived class里面的mf1与mf3会遮挡base class里面的同名函数
class Derived: pubcic Base{
public:
	virtual void mf1();
	void mf3();
	void mf4();
};

//此时 
Derived d;
int x;
d.mf1();		//调用Derived::mf1
d.mf1(x);		//本意调用Base::mf1,但是被遮挡了,所以错误

但是可以使用using声明和转交函数来显示出base classes的名称。

class Base{
private:
	int x;
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
	virtual void mf2();
	void mf3();
	void mf3(double);
};

//如果不使用using声明derived class里面的mf1与mf3会遮挡base class里面的同名函数
class Derived: pubcic Base{
public:
	using Base::mf1;		//使用using声明让base class里面名为mf1与mf3的所有东西在derived class可见
	using Base::mf3;		//并且是public的
	virtual void mf1();
	void mf3();
	void mf4();
};
//上面的using声明式也放在derived class的public的区域
//因为base class内的public名称也应该是derived class的public。
//这个符合is-a关系。

但是有时候你不想继承base class中所有的函数,这种情况一般是在private继承下发生。

比如Deried class以private继承Base class,而Derived唯一想要继承的是mf1的无参数版本,这样的话,使用using声明就不合适,因为这样用Derived class就能访问所有的mf1版本。此时,我们可以使用转交函数。

class Base{
public:
	virtual void mf1() = 0;
	virtual void mf1(int);
};

class Derivedprivate Base{
public:
	virtual void mf1()		//转交函数
	{ Base::mf1(); }		
};

区分接口继承与实现继承

对于class的设计者,有的时候我们只希望继承成员函数的接口,有时候我们希望同时继承函数接口和实现并覆写它们所继承的实现。有的时候我们又希望继承函数的接口与实现,并且不允许覆写任何东西。

在public的条件下,public继承意味着is-a,所以public继承的成员函数接口总是会被继承。

声明一个pure virtual函数就是为了让derived class只继承函数的接口。

声明一个virtual函数(非纯虚函数)的目的就是为了让derived class继承该函数的接口和缺省实现。

声明一个non-virtual 函数就是为了令derived继承接口和一个强制性的实现。

考虑virtual函数以外的其他选择

书上说这部分是为了让你跳脱面向对象设计的常轨,考虑一些其他的解法。

class GameCh{
public:
	virtual int healthVal() const;
};

由Non-Virtual Interface 实现 Template Method模式

class GameCh{
public:
	int heathVal() const
	{
		...							//这里可以做一些事前工作
		int retVal = doHealthVal();
		...							//这里可以做一些事后工作
		return retVal;
	}
private:
	virtual int doHealthVal() const
	{...}
};

实际上就是保留一个public的成员函数,并让该成员函数成为non-virtual,该成员函数调用一个private virtual函数进行实际工作。

代码的事前工作与事后工作来保证virtual函数在被调用之前作某些事情(锁定互斥锁,调用运转记录的日志等),在被调用之后又做了某些事情(解除互斥锁等)。如果让客户直接调用virtual函数的话就没有任何好办法可以做这件事。

NVI手法会在derived class中重新定义private virtual函数。

NVI手法中的virtual函数并不一定是private,也可以是protected,但是virtual一定是public的时候,就不能使用NVI手法了。
Function Pointers实现 Strategy 模式
NVI手法依然还是使用virtual来进行实现的。
还有一种设计思路,就是主张doHealthVal的计算是完全与不同的子类无关,我们可以让每一个子类的构造函数都接受一个指针,这个指针是指向一个计算函数。

class GameCh;			//前置声明
int defaultHealthCalc(const GameCh& gc);	//计算健康值的默认缺省算法
class GameCh{
public:
	typedef int (HealthCalcFuc)(const GameCharacter&);		//C++11后用using替代或std::function替代该语句,如下
	//using HealthCalcFunc = int (*)(const GameCh&);
	//using HealthCalcFunc = std::function<int(const GameCh&)>;
	explicit GameCh(HealthCalcFuc hcf = defaultHealthCalc)
		:healthFuc(hcf)
	{}
	int healthValue() const
	{ return healthFuc(*this); }
	...
private:
	HealthCalcFuc healthFuc;
};

class EvilBadGuy: public GameCh{
public:
	explicit EvilBadGuy(HealthCalcFuc hcf = defaultHealthCalc)
		:GameCharacter(hcf)
	{...}
	...
};

int loseHeathQuickly(const GameCh&);		//计算方式1
int loseHeathSlow(const GameCh&);			//计算方式2

EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthQuickly);			//相同类型的人物搭配,不同的计算方式

上述typedef那段代码不好阅读,详见下面链接。
参考链接
使用这种方法的话,如果人物的计算函数在运行期间需要变化,完全可以在GameCh中提供一个成员函数SetHealthCalc来替换当前的健康指数。

这些健康指数计算函数不再是GameCh的成员函数,也并没有特别访问即将计算的那个对象里面的成分。

当然,这些计算函数需要的只是GameCh里面的public的信息,但如果,他们需要的信息是non-public的信息,他们不是成员函数,无权访问,这就麻烦了。

唯一解决的方法就是弱化class的封装,比如使用那个函数作为friend函数或是为其实现提供public的访问函数之类的。

由tr1::function 完成Strategy模式

如果熟悉template以及它们对于隐式接口的使用,基于函数指针的做法就有点死板。应该可以更灵活点。

我们可以使用tr1::function对象来去除约束。

class GameCh;
int defaultHealthCalc(const GameCh& gc);
class GameCh{
public:
	//此时具有很大的弹性,接受一个reference指向const GameCh并返回int
	using HealthCalcFunc = std::tr1::function<int(const GameCh&)>;
	explicit GameCharacter(HealthCalcFunc hfc = defaultHealthCalc)
		:healthFuc(hcf)
	{}
	int healthVal() const
	{ return healthFunc(*this); }
private:
	HealthCalcFunc healthFunc;
};

有非常大的弹性,配合std::tr1::bind甚至能让计算函数使用成员函数。(详细见书,不过书上没写细节)

古典Strategy模式
古典的Strategy模式会把计算函数做成一个分离的继承体系的virtual成员函数。

class GameCharacter;
class HealthCalcFuc{
public:
	virtual int cacl(const GameCh& gc) const
	{...}
};

HealthCalcFuc defaultHealthCalc;

class GameCh{
public:
	explicit GameCh(HealthCalcFuc* phcf = &defaultHealthCalc)
		:pHealthCalc(phcf)
	{}
	int healthVal() const
	{ return pHealthCalc->calc(*this);}
private:
	HealthCalcFuc* pHealthCalc; 
};

这个解法的关键在于,很容易辨认(熟悉Strategy模式),并且只要为HealthCalcFuc添加一个derived class,就可以将一个既有的算法纳入使用。

本章好麻烦。
但是介绍了几个替代virtual的方案:
NVI手法
Function Pointer手法
tr1::function成员变量替代手法
古典Strategy手法

绝不重新定义继承来的non-virtual函数

因为non-virtual 就是为了在继承体系里面保持一个不变性。

绝不重新定义继承而来的缺省参数值

virtual 函数是动态绑定,而缺省参数值是静态绑定。

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(Shape color) const;	//指定参数值
	//此处一定要写上参数值,因为静态绑定下这个函数并不从其base继承缺省参数值,
	//但是以pointer或reference调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省实现
};


Shape* ps;				   //静态类型为Shape*
Shape* pc = new Circle;	   //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
//因为都声明为Shape*,所以他们的静态类型为Shape*

//动态类型是指目前指向对象的类型(通常经由赋值动作)
ps = pc;					//ps的动态类型为Circle*
ps = pr;					//pr的动态类型为Rectangle*

pc->draw(Shape::Red);		//调用Circle::draw(Shape::Red)
pr->draw(Shape::Red);		//调用Rectangle::draw(Shape::Red)

//调用这个缺省值是来自与Shape的,并不是Rectangle的Green!!
pr->draw();					//调用Rectangle::draw(Shape::Red)!
//

反正绝对不要重新定义一个继承而来的缺省参数,因为缺省参数都是静态绑定,而我们唯一能覆写的是virtual函数–动态绑定。

根据复合塑模出has-a或是根据某物实现出

复合是类型之间的一种关系,当某种类型的对象内含它种类型的对象,就是复合关系。

class Address{...};
class PhoneNum{...};
class Person{
public:
private:
	Address address;
	PhoneNum phonenum;
};

上述表现出来的关系就是复合,代表的是has-a关系。
复合具有两种意义:一种是has-a,另外一种就是根据某物实现出。
has-a的表现在于应用域中(对象是你塑造的世界中的某种事物),而根据某物实现出就是表现在与实现域中(对象是纯粹的实现细节,如互斥锁、缓冲区)。

复合的概念与public的概念是完全不同的。

明智地使用private继承

如果class之间是一个private继承关系,那么,就不会在将derived class对象转换成一个base class对象。

Private在软件设计层面上没有太多的意义,其意义主要在实现的层面,表现出来的意义是is-implemented-in-terms-of(根据某物实现出)。与复合一样。

只有当必要的时候我们才使用private继承。比如涉及到protected成员与virtual函数的时候,或是当空间方面的利害关系足以踢翻private继承支柱的时候。

class Timer{
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;
};

//为了让Widget不能隐式转换为Timer,可以有一下两种方法
//即让Widget获得Timer的所有实现但是不继承接口

//借由private继承,Timer的public onTick在Widget变成private
class Widget: private Timer{
private:
	virtual void onTick() const;
};
//使用一个新的类和public继承来复合实现
class Widget{
private:
	class WidgetTimer: public Timer{
		public:
			virtual void onTick() const;
			..
	};
	WidgetTimer timer;
};

private意义

使用复合替换Private继承的理由:
1、你想使Widget拥有derived class,但是你却不想derived class重新定义onTick()
2、你想Widget的编译依存关系降到最低。

使用Private继承的一些理由:
1、当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分。
2、为了重新定义一个或多个virtual函数。
3、激进的涉及空间最优化问题。

//激进的问题	
class Empty{};			//这个类没有数据,现实中这个类可能没有non-static成员变量,却往往内含有typedef,enums,static成员便变量或非virtual函数

class HoldAnInt{
private:
	int x;
	Empty em;			//这个是会要求内存的,即便真的没有数据
};

//可以使用private进行空白基类最优化(EBO),但只能在单一继承中可行,并非多重继承。而且EBO无法被施用与有多个base class的derived class上
class HoldAnInt: private Empty{
private:
	int x;
};

明智地使用多重继承

多重继承相对来说比较复杂,所以我选择单一继承。

virtual
virtual
A
B
C
D

上图所示的是一个要命的钻石多重继承UML图,因为不知到怎么画会比较好看,所以就将就一下。

多重继承的话,如果是使用non-virtual继承的话,D里面有成员变量,那么继承下来的A会有两份成员变量。解决方法是把D变成一个virtual base class。

书上对于virtual base class的忠告是:如果是不必要的情况就不要使用virtual base class。如果必须使用的话,就必须避免在里面存放数据。

后面由举了一个多重继承的正当用途的例子

private
A
B
C

即让B成为纯接口类,让C保留存放的数据,并为private继承(C为协助实现的class)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值