高效C++编程学习笔记(六)

继承于面向对象设计

条款32.确定你的public继承塑模出is-a关系

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

条款33.避免遮掩继承而来的名称

该题材主要与作用域有关,一般来说derived class作用域被嵌套在base class作用域内。

class Base{
	private:
		int x;
	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()首先会在class Derived覆盖的作用域找,然后是在Base中找,都没有的话在含Base的那个namespace(s)的作用域(如果有的话),最后在global的作用域找。

对上诉例子进行部分改动,添加mf3重载,重新充分认识继承体系内的“名称可视性”

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 mf4();
	void mf4();
	...
};

当如下调用时会出现base class中的mf1与mf3被derived class内的mf1和mf3函数遮掩掉了。

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和Derived classes之间的is-a关系,在派生类中无法使用基类函数。不过可以使用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:
	using Base::mf1;		//让Base class内名为mf1和mf3的所有东西在Derived作用域可见(并且都为public)
	using Base::mf3;
	virtual void mf1();
	void mf4();
	void mf4();
	...
};

当并不希望继承base classes的所有函数时,使用private继承,对于需要继承的函数使用转交函数(forwarding function):

class Base{
	private:
		int x;
	public:
		virtual void mf1() = 0;
		virtual void mf1(int);
		...		//与前同
};
class Derived:private Base{
public:
	virtual void mf1()	//转交函数,暗自成为inline
	{Base::mf1();}
	...
};

条款34.区分接口继承和实现继承

* 声明一个pure virtual函数的目的就是为了让derived classes只继承函数接口
* (非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。

impure virtual函数同时指定函数声明和缺省行为,可能造成危险。
在基类中的impure virtual函数会在derived class中不定义时会默认缺省。可能在未知的方式被调用。
需要切断“virtual 函数接口”和其“缺省实现”之间的连接。最好的方法是实现纯虚函数的自定义,举一个例子:
定义不同飞机的飞行方式。A,B一直使用默认的方式,C有特有的方式

class Airplane{
public:
	virtual void fly(const Airport& destinaton) = 0;
	...
};
void Airplane::fly(const Airport& destination)	//纯虚函数的实现
{
	缺省行为
}
class ModelA:public Airplane{
public:
	virtual void fly(const Airport& destination)
	{Airplane::fly(destination);}
	...
};
class ModelB:public Airplane{
public:
	virtual void fly(const Airport& destination)
	{Airplane::fly(destination);}
	...
};
class ModelC:public Airplane{
public:
	virtual void fly(const Airport& destination);
	...
};
void ModelC::fly(const Airport& destination)
{
	C型飞机特有的飞行方式
}

此时,通过对纯虚函数的定义,A,B可以使默认的实现接口,C需要手动去实现。实现了接口与实现分离。此时声明部分表现的是接口(用于derived class使用),定义部分实现缺省行为,(derived class可以使用缺省行为,但是只有明确提出申请时才行)。

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class接口
  • pure virtual函数值具体指定接口继承
  • impure virtual函数具体指定接口继承及缺省实现继承
  • non-virtual 函数具体指定接口继承以及强制性实现继承

条款35:考虑virtual函数以外的其它选择

以游戏角色健康值为例,不同的角色健康值不同,可能第一想到的是用virtual函数去实现。看下以下几种替代方法。
在解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方法。有以下几种替代方法

  • 使用non-virtual interface(NVI)手法,那是Template Method设计模式中的一种特殊形式。用public non-virtual 成员函数包裹较低访问性的virtual函数。
class GameCharacter{
	public:
		int healthValue() const		//devired classes不重新定义
		{
			...						//做一些事前工作
				int retval = doHealthValue();				
			...						//做一些事后工作
				return retVal;
		}
	private:
		virtual int doHealthValue()const		//devired classes重新定义
		{
			...
		}
};

优点是可以在事前事后的操作中省略重写代码,比如加解锁,日志记录,验证class约束条件

  • 将virtual函数替换为“函数指针成员变量”,是Strategy设计模式的一种分解表现形式。
class GameCharacter;	//前置声明
int defaultHeathCalc(const GameCharacter& gc);
class GameCharacter{
public:
	typedef int (*HealthCalcFunc)(const GameCharacter&);
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
	:healthFunc(hcf)
	{}
	int healthValue()const
	{return healthFunc(*this);}
	..
	private:
		HeallthCalcGunc healthFunc;
};

相比于virtual的做法,提供了弹性

  1. 同一个人物类型之不同实体可以有不同的健康计算函数。例如
class EvilBadGuy:public GameCharacter{
	public:
		explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
		:GameCharacter(hcf)
		{...}
		...
};
int loseHealthQuickly(const GameCharacter&);	//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2
//相同类型的人物搭配不同的健康计算方式
EvilBadGuy ebg1(loseHealthQuickly);		
EvilBadGuy ebg2(loseHealthSlowly);				
  1. 对某已知人物之健康指数计算函数可以在运行期变更。例如在GameCharacter中提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
  • trl::function成员变量替换virtual函数。允许使用任何的可调用物搭配一个兼容于需求的签名式。
    与使用函数指针不同,而是改用一个类型为trl::function的对象,相比于函数指针,该对象可以是某个“像函数的东西”,且返回类型是任何可悲转换为int的类型。
class GameCharacter;	//如前
int defaultHeathCalc(const GameCharacter& gc);	//如前
class GameCharacter{
public:
	//HealthCalcFunc可以是任何“可调用物”(callable entity),可被调用并接受任何兼容于GameCharacter之物,返回任何兼容于int的东西
	typedef std::trl::function<int (const GameCharacter&)>HealthCalcFunc
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
	:healthFunc(hcf)
	{}
	int healthValue()const
	{return healthFunc(*this);}
	..
	private:
		HeallthCalcGunc healthFunc;
};

具体观察HealthCalcFunc。
std::trl::function<int (const GameCharacter&)>
对比于函数指针,这个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”,但与函数指针不同的是,返回类型兼容能所有隐式转换为int类型,入参也可以是非const类型,这种范式行为带来了极大的方便。

  • 将继承体系内的virtual函数替换为另外一个继承体系中的virtual函数。这个是Strategy设计模式的传统实现手法。对应的代码如下:
class GameCharacter;		//前置声明
class HealthCalcFunc{
public:
	...
virtual int calc(const GameCharacter& gc) const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
	explicit GameCharacter(healthCalcFunc* phcf = &defaultHealthCalc)
	: pHealthCalc(phcf)
	{}
	int healthValue()const
	{return pHealthCalc->calc(*this);}
	...
private:
	HealthCalcFunc* pHealthCalc;
};

其中GameCharacter是某个继承体系的根类,其可以有derived classes 分别为EvilBadGuy与EyeCandyCharacter,HealthCalcFunc是另一个继承体系的根类,体系中有derived classes分别为SlowHealthLoser和FastHealthLoser,每一个GameCharacter对象中都内含一个指针,指向来自HealthCalcFunc继承体系的对象。

条款36:绝不重新定义继承而来的non-virtual函数

该条款很好理解,即重新定义的non-virtual函数会破坏父类的定义,违背了条款32与34

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

该条例重点讨论“继承一个带有缺省参数值的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(ShapeColor color) const;
		//以上这么写当客户以对象调用此函数是,一定要指定参数值。因为静态绑定下这个函数并不从其base继承缺省参数值。
		//但以指针(或reference)调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值。
};

静态绑定为前期绑定

//静态类型都为Shape*
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

动态类型为延迟绑定

ps = pc;	//ps的动态类型如今是Circle*
ps = pr;	//ps的动态类型如今为Rectangle*

简单来说,就是当以对象调用的时候derived classes对象会无法继承base类的缺省参数。而通过指针调用时。调用相应的draw函数时缺省参数因为是静态绑定所以会使用base类的缺省参数而不是自身的。

这种现象导致base缺省参数的改变会导致其derived classes类的改变。

解决的方法如条款35所示的几种方法:
以NVL为例

class Shape{
	public:
		enum ShapeColor{Red,Green,Blue};
	    void draw(ShapeColor color = Red) const;	//为一个non-virtual
	    {
	    	doDraw(color);				//调用一个virtual
	    }
		...
	private:
		virtual void doDraw(ShapeColor color)const = 0;	//真正工作在此完成
};

class Rectangle: public Shape{
	public:
		...
	private
		virtual void doDraw(ShapeColor color)const;	
};

条款38:通过复合塑模出“has-a”或“根据某物实现”

复合塑模即在一个class中包含其他的class,如人至少有一个名字,地址和身份号码,这个就是has-a,而不会说成人是一个名字,地址和身份号码的is-a形式。
而对于“根据某物实现”,举一个例子:
用一组class来表现由不重复对象组成的sets;由于set template中“每个元素耗用三个指针”,会消耗大量空间,节省时间。但对空间约束比较严格时,就无法使用set template。而list template与set类似,但由于list能容纳重复的事物,因此它们的关系并非是is-a的关系,无法使用public继承来塑模它们。set需要“根据list来”实现。

template<class T>
class Set{
public:
	bool member(const T& item) const;
	void insert(const T& item);
	void remove(const T& item);
	std::size_t size()const;
private:
	std::list<T> rep;		//用来表述Set的数据		
};
//Set成员函数可大量依赖list及标准程序库其他部分提供的机能来实现
template<typename T>
bool Set<T>::member(const T& item)const
{
	return std::find(rep.begin(),rep.end,item)!= rep.end();
}
template<typename T>
void Set<T>::insert(const T& item)
{
	if(!member(item)) rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item)
{
	typename std::list<T>::iterator it = std::find(rep.begin(),rep.end,item);
	if(it != rep.end())
		rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size()const
{
	return rep.size();
}

条款39:明智而谨慎地使用private继承

不同于public继承的is-a形式,private继承意味着条款38的“根据某物实现”。因此继承自private base class的每样东西在你的class中都是private:因为它们都是实现枝节而已。
因此尽量使用条款38的复合必要时才使用private。

  • 通常比复合(composition)的级别低。但是当derived class 需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的
  • 和复合(composition)不同,private继承可以照成empty base最优化。即所谓的EBO(empty base optimization;空白基类最优化)

条款40:明智而谨慎地使用多重继承

多重继承会导致很多隐藏性的问题,

  • 例如多重继承的类中都有相同名称的成员变量或者函数时。调用会产生歧义。在调用时需要明确指出调用的是哪一个base class的函数。
  • 当多重继承的类又同一个基类时,会产生“钻石型多重继承”,而不想继承复制多份时,需要直接继承最高base的derivedclasses采用“virtual继承”
class File{...};
class InputFile:virtual public File{...};
class OutputFile:virtual public File{...};
class IOFile:public InputFile,public OutputFile
{...};

virtual继承所产生的体积大且初始化的规则远比non-virtual bases的情况复杂且不直观,若果virtual base 不带任何数据,将具有实用价值的情况。

关于多重继承的正当用途,涉及到“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合的情况
具体实例可以看书中IPerson,PersonInfo,CPerson的例子。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值