effective C++笔记--继承与面向对象设计(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/rest_in_peace/article/details/84303600

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

. public继承在父类和子类之间的关系应该是:子类的对象也是一个父类的对象,但是父类的对象不是子类的对象,通俗点讲就是父类能派上用处的地方,子类也能派上用处,但是反过来就不是了。
  假设有一个类表示鸟,它能派生出表示企鹅的类,鸟可以飞,所以在鸟这个类中有一个方法是fly,但是企鹅应该是不会飞的,这就对上述叙述带来疑惑,所以在设计继承体系的时候应该有更多的考虑,比如将鸟分为会飞的和不会飞的两个类来表示。
  is-a并非唯一存在与class之间的关系,另外的常见的关系有has-a和is-implemented-in-terms-of(根据某物实现出)。

避免遮掩继承而来的名字

. 如同局部变量在局部作用域中使用的时候会覆盖掉外部变量,这点在继承体系中也很相似的:子类的同名变量将会覆盖父类中所有同名变量。因为子类的作用域就像是嵌在父类的作用域中的。

class Base{
private:
	int x;
public:
	virtual void f1() = 0;
	virtual void f1(int);
	void f2();
	void f2(int);
};
class Derived:public Base{
public:
	virtual void f1();
	void f2();
	void f3();
};

Derived d;
int x;
...
d.f1();			//正确,调用Derived::f1()
d.f1(x);		//错误,因为Derived::f1遮掩了Base::f1
d.f2();			//正确,调用Derived::f2()
d.f2(x);		//错误,因为Derived::f2遮掩了Base::f2

. 以作用域为基础的“名称掩盖规则”并没有被改变,因此Base类中名为f1和f2的函数都被Derived类中的f1和f2遮掩掉了,就像没有继承这两个函数一样。
  如果正在使用public继承但是又不想继承那些重载的函数,可以使用using声明式达到目的:

class Base{
private:
	int x;
public:
	virtual void f1() = 0;
	virtual void f1(int);
	void f2();
	void f2(int);
};
class Derived:public Base{
public:
	using Base::f1;
	using Base::f2;
	virtual void f1();
	void f2();
	void f3();
};

Derived d;
int x;
...
d.f1();			//正确,调用Derived::f1()
d.f1(x);		//正确,调用Base.f1(int)
d.f2();			//正确,调用Derived::f2()
d.f2(x);		//正确,调用Base.f2(int)

. 有时候可能并不像继承所有的函数,当然这在public继承中不可能发生,因为public继承是一种is-a的关系。然而在private继承下,假设Derived唯一想继承的是一个无参数的版本,using声明在这里将用不上,因为using声明会将所有同名函数在Derived class中都可见。可以通过一个简单的转交函数来实现:

class Base{
private:
	int x;
public:
	virtual void f1() = 0;
	virtual void f1(int);
	...
};
class Derived:private Base{
public:
	virtual void f1(){
		Base::f1();
	}
	...
};

Derived d;
int x;
...
d.f1();			//正确,调用Derived::f1()
d.f1(x);		//错误,Base::f1(int)被遮盖

区分接口继承和实现继承

. 在基类中声明函数的方式有三种,可以是纯虚函数,可以是虚函数或者是一个非虚的函数,比如:

class Shape{
public:
	virtual void draw() const = 0;				//隐喻画出对象
	virtaul void error(const std::string& msg);	//报告错误
	int objectID() const;						//返回当前对象的识别码
	...
};

class Rectangle : public Shape{...};
class Ellipse : public Shape{...};

. Shape是一个抽象类,所以不能为它创建实体,只能创建它的派生类的实体,但是它还是强烈的影响了所有以public形式继承了它的派生类,因为:
  成员函数的接口总是被继承。 public继承意味着is-a关系,所以发生对基类为真的事情一定也对派生类为真。
  声明一个纯虚函数的目的是为了让派生类只继承函数接口。 纯虚函数有两个最突出的特性:它们必须被任何继承了它们的具象类重新声明;它们在抽象class中通常没有定义。如上面的代码,画出抽象的形状是不合理的,但是可以画出具体的如矩形或是圆形,因此Shape::draw的声明式就像在对派生类的设计者说:你必须提供一个draw函数,但我不干涉你怎么实现它。
  声明一个非纯的虚函数的目的是让派生类继承该函数接口和缺省的实现。 派生类会继承基类的非纯的虚函数的函数接口,但是通常它会提供一份实现代码,派生类可能会覆写它。比如上面的代码,error函数接口表示每个class都应该支持遇上错误可调用的函数,但每个class可自由处理错误,如果某个class不需要对错误做特殊的处理,它可以退回到Shape class提供的缺省的错误行为,因此Shape::error的声明式就像在对派生类的设计者说:你必须提供一个error函数,但是如果你不想自己写一个,可以使用Shape class提供的版本。
  声明一个非虚函数意味着它不打算在派生类中有不同的行为。 实际上一个非虚成员函数所表现的不变形凌驾与特异性,因为他表示不论派生类变得多么特异化,它的行为都不能改变。如以上的代码,Shape::objectID表示:每个Shape派生类对象都有一个用来产生对象识别码的函数,此方法应该保持一致,任何派生类都不应该尝试改变他的行为。

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

. 假设设计的一个游戏中,编写了一个游戏人物类,其中有名为healthValue的成员函数来表示人物的健康值,不同的人物应该有不同的方式爱计算健康值,因此将这个成员函数声明为virtual的似乎是再明白不过的方法了:

class GameCharacter{
public 
 virtual int healthValue() const;			//不是纯虚函数表示有缺省的计算方法
 ...
};

为了跳出面向对象设计时的常规思路,可以考虑一些其他的方法:

由Non-Virtual Interface(NVI)手法实现Template Method模式(模板方法模式):

. 有一个有趣的思想流派主张:virtual函数应该总是private的。这个流派的拥护者建议,较好的设计是保留healthValuee为public成员函数,并且是非虚函数,通过调用一个private的虚函数来完成实际工作,比如:

class GameCharacter{
public 
	int healthValue() const{
		...
		int ret = doHealthValue();
		...
		return ret;
	}
	...
private:
	virtual int doHealthValue() const;
 ...
};

. 这一基本设计就是通过共有的非虚成员函数间接调用private 虚函数。这么做有一个优点,即可以在共有函数中,调用虚函数之前和之后都做一些相关工作,这意味着这个非虚函数可以确保一个virtual函数在调用前设定好适当的场景,并在调用后清理场景。事前工作包括锁定互斥器、验证函数先决条件等,事后工作包括解锁互斥器、验证函数事后条件等。
  但是有时候要求virtual函数必须是public的,这样就没办法使用NVI手法了。

由Function Pointer实现Strategy模式(策略模式):

. 有一种思路可以让让每个人物的构造函数中接受一个指针,指向一个计算健康值的函数,可以通过调用这个函数完成实际的计算:

class GameCharacter;						//前置声明
//计算健康值的默认函数
int defaultHealthCalc(const GameCharacter& );
class GameCharacter{
public:
	typedef int (HealthCalcFunc)(const GameCharacter&);		//定义函数指针
	explicit  GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
		:healthFunc(hcf){}
	int healthValue() const{
		return healthFunc(*this);
	}
	...
private:
	HealthCalcFunc healthFunc;
};

. 通过在构造的时候传入不同的计算健康值的函数也可以给不同的人物设置不同的计算健康值的方法,并且这个函数在继承体系之外,不会访问到类的非公有部分,如果人物的健康值能只通过公有信息就能计算得到,那就没什么问题,但是如果需要非公有信息进行精确计算的时候,就会有问题了,解决办法是弱化类的封装性,这代价是否值得需要好好考虑。

由tr1::function完成strategy模式:

.  tr1::function是一个类模板,它与函数指针很像,但是因为重载了(),所以看上去比函数指针更易使用,而且函数指针智能绑定外部的函数,而tr1::function可以绑定任何类型的函数,其形式如:function<int (const GameCharacter&)>,尖括号的前一个参数表示返回值,后一个用括号括起来的表示参数类型,且这两个类型都具有兼容性,即可调用物的参数可以被隐式转换成const GameCharacter& ,返回值可被隐式转换为int。之前的代码可以改为:

class GameCharacter;						//前置声明
//计算健康值的默认函数
int defaultHealthCalc(const GameCharacter& );
class GameCharacter{
public:
	typedef std::tr1::function<int (const GameCharacter&) > HealthCalcFunc;
	explicit  GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
		:healthFunc(hcf){}
	int healthValue() const{
		return healthFunc(*this);
	}
	...
private:
	HealthCalcFunc healthFunc;
};

. 代码的改变很小,但是为程序带来了巨大的弹性,tr1::function对象可以指向一个函数、一个函数对象或是一个成员函数:

short calcHealth(const GameCharacter&);		//健康计算函数

struct HealthCalculator{						//函数对象
	int operator()(const GameCharacter&) const{
		...
	}
};

class GameLevel{
public:
	float health(const GameCharacter&) const;		//成员函数
	...
};

class GoodGuy : public GameCharacter{
	...
};

class BadGuy : public GameCharacter{
	...
};

GoodGuy gg1(calcHealth);					//人物1,使用函数计算健康值
BadGuy bg1(HealthCalculator());			//人物2,使用函数对象计算健康值
GameLevel gl;
GoodGuy gg2(
		std::tr1::bind(&GameLevel::health,
					gl,
					_1));				//人物3,使用成员函数计算健康值

. 古典strategy模式: 还可以将计算健康值的这一做法抽象为一个类,将不同的计算方法都作为这个做法的派生类,然后在人物类中包含一个指向这个计算健康值的基类的指针作为成员属性:

class GameCharacter;						//前置声明
class HealthCalcFunc{
public:
	...
	virtual int calc(const GameCharacter& gc) const{
		...
	}
	...
};
HealthCalcFunc defaultCalc;
class GameCharacter{
public:
	explicit  GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
		:pHealthClac(phcf){}
	int healthValue() const{
		return pHealthClac->clac(*this);
	}
	...
private:
	HealthCalcFunc* pHealthCalc;
};

. 这样做之后,还有什么不同的计算方式,只要再为这个集成体系声明一个派生类即可。

猜你喜欢

转载自blog.csdn.net/rest_in_peace/article/details/84303600