Effective C++学习笔记六:继承与面向对象设计

三十二:Make sure public inheritance models "is-a"确定你的public继承塑模出Is-a关系

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

三十三:Avoid hiding inherited names。 避免遮掩继承而来的名称

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

int main()
{
	Derived d;
	int x;
	d.mf1();
	d.mf1(x);//错误 因为Derived::mf1遮掩了Base::mf1
	d.mf2();
	d.mf3();
	d.mf3(x);//错误 因为Derived::mf3遮掩了Base::mf3
}

1.可以using声明式达成目标,解除遮掩。

2.使用转交函数

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

class Derived :private Base {
public:
	virtual void mf1()//转交函数
	{
		Base::mf1();//暗自成inline
	}
}

注:

derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

三十四:区分接口继承和实现继承

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

声明非纯虚函数的目的是为了让派生类继承该函数的接口和缺省实现。

声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

它绝不该在derived class中被重新定义。

避免设计错误1:将所有函数声明为non-virtual

                       2:将所有成员函数声明为virtual。

注:

1.接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

2.pure virtual函数只具体指定接口继承。

3.非纯虚函数指定接口继承及缺省实现继承。

4.非虚函数具体指定接口继承以及强制性实现继承。

三十五:Consider alternatives to virtual functions。考虑virtual函数以外的其他选择

例子:

class GameCharacter {
public:
	virtual int healthValue() const;//返回人物的健康指数。 derived classes可重新定义它。
};

1.借由Non-Virtual Interface手法实现 Template Method模式。

一个流派主张virtual函数几乎总是private。

class GameCharacter {
public:
	int healthValue() const   //derived classes 不重新定义它
	{
		//...  做一些事前工作,详下
		int retVal = doHealthValue();//做真正的工作
		//...  做一些事后工作
		return retVal;
	}
private:
	virtual int doHealthValue() const //derived classes可重新定义它
	{
		//...                          缺省算法,计算健康指数
	}
};

这个令客户通过public non-virtual成员函数间接调用private virtual 函数,称为non-virtual interface(NVI)手法。

我们把这个non-virtual函数称为virtual函数的外覆盖器。

NVI手法的一个好处是上面所谓的事前工作和事后工作,一些不适合让客户做的通用的准备和回收动作。

2.借由Function Pointers实现Strategy模式

class GameCharacter;//前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(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:
	HealthCalcFunc healthFunc;
};

这是一个简单的策略模式,

特点:同一人物类型的不同实体可以有不同的健康计算函数。

           计算方式可以在运行期间变更。

但是访问class的non-public部分会有问题,解决方式是弱化class的封装,包括友元或提供public访问函数。权衡优缺点选择设计模式。

3.借由tr1::function 完成Strategy模式

不使用函数指针,改用一个类型为tr1::functoin的对象。

class GameCharacter;//前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
	//typedef int(*HealthCalcFunc)(const GameCharacter&);
	typedef std::tr1::function<int(const GameCharacter&)> HealthCalcFunc;
	explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
		:healthFunc(hcf)
	{

	}
	int healthValue() const
	{
		return healthFunc(*this);
	}
private:
	HealthCalcFunc healthFunc;
};

可调用五的参数可被隐式转换为const GameCharacter&, f返回类型可被隐式转换为int。

4.古典的策略模式

将计算方法包装成一个类,计算方法变虚函数,通过传递类指针计算。

注:virtual函数的替代方案包括NVI手法以及策略模式的多种形式

将机能从成员函数转移到class外部函数,缺点:非成员函数无法访问class的non-public成员。

tr1:fucntion 对象的行为就像一般函数指针。这样的对象可接纳与给定目标签名兼容的所有可调用物。

三十六:Never redefine an inherited non-virtual function绝不重新定义继承而来的non-virtual函数

动态绑定和静态绑定的区别。

三十七:Never redefine a function's inherited default parameter value. 绝不重新定义继承而来的缺省参数值。

同上:即缺省参数值是静态绑定

例:

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;
	//以对象调用此函数,一定要指定参数值(静态绑定不继承)。  指针或引用调用可以不指定参数值(动态绑定会继承)
};

veryimportant:

Shape* ps;//静态类型为Shape*

Shape* pc = new Circle;//静态类型为Shape*

Shape* pr = new Rectangle;//静态类型为Shape*

对象所谓的动态类型是指“目前所指对象的类型”,动态类型可以表现出一个对象将会有什么行为。

pc 的动态类型为Circle*,  pr的动态类型为Rectangle*,ps没有动态类型。

动态类型可以在程序执行过程中改变。

virtual函数是动态绑定而来,调用一个virtual函数,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。

在考虑上面的例子:

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

因为一个是动态绑定 一个是静态绑定 各取一半。

这样做的原因是为了运行期间效率。

可以用NVI替代设计:

class Shape
{
public:
	enum ShapeColor {Red, Green, Blue};
	void draw(ShapeColor color = Red) const
	{
		doDraw(color);
	}

private: 
	virtual void doDraw(ShapeColor color) const;
};

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

注:

绝对不要重新定一个继承而来的缺省参数值,因为缺省参数值是静态绑定,而virtual 函数是动态绑定。

三十八:Model  "has-a" or "is-implemented-in-terms-of" through composition通过复合塑模出has-a 或 “根据某物实现出”

复合结构 对应 继承结构 各有优劣。

例子:

class Address {};
class PhoneNumber {};

class Person
{
public:
	//
private:
	std::string name;
	Address address;
	PhoneNumber voiceNumber;
	PhoneNumber faxNumber;
};

三十九:Use private inheritance judiciously. 明智而审慎地使用private继承

1.如果class之间地继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。

2.由private base class继承而来的所有成员,在derived class中都会变成private属性。

Private继承意味着implemented-in-terms-of(根据某物实现出)。

尽可能使用复合,必要时才使用private继承,何时才是必要?主要是当protected成员或virtual函数牵扯进来时。

例子:

class Timer
{
public:
	explicit Timer(int tickFrequency);
	virtual void onTick() const;   //定时器每滴答一次,此函数就被自动调用一次。
};

现在我们的widget类需要记录每个成员函数的被调用次数,我们可以继承TImer。这里public继承不大恰当,因为widget不是一个TImer,我们只需要TImer的tick fuction。我们以private继承。

class Widget : private Timer {
private:
	virtual void onTick() const; //查看widget的数据
};

在这里把onTick放进public接口内会误导客户端以为他们可以调用它。

同样的我们可以以复合模式设计这个类:

class Widget
{
private:
	class WidgetTimer :public Timer {
	public:
		virtual void onTick() const;
	};
	WidgetTimer timer;
};

我们可能更愿意这种public加复合的方式。

1.如果考虑widget有派生类 并且阻止derivedclasses 重新定义onTick。 c++可以通过上面这种方式模拟阻止derived classes重新定义它的virtual函数。

2.将widget的编译依存性降至最低。 如果Widget 继承 Timer, 当Widget被编译时TImer的定义必须可见,所以定义Widget的那个文件恐怕必须#include TImer.h。但如果WidgetTimer移除Widget之外而Widget内含指针指向一个WidgetTimer, Widget可以只带着一个简单的WigdetTimer声明式。 

两个方式的另外一个情况:

class Empty {};       //没有数据,所以其对象应该不使用任何内存

class HoldsAnInt {   //应该只需要一个int空间
private:
	int x;
	Empty e;         //应该不需要任何内存
};

然而sizeof(HoldsAnInt) > sizeof(int);因为大小为零的独立对象,通常会插入一个char到空对象内。然而又因为内存对齐的原因,上面可能会被填充到一个int大小。

但是,对于

class HoldsAnInt : private Empty {
private:
	int x;
};

sizeof(HoldsAnInt) == sizeof(int) 。 这就是所谓的EBO(empty base optimization; 空白基类最优化)。EBO一般只在单一继承下才可行。

尽管如此,大多数classes并非empty。大多数继承相当于Is-a,这里指public继承,不是private继承。复合和private继承都意味

is-implemented-in-terms-of,但复合比较容易理解。所以尽量使用复合。

注:

Private继承意味根据某物实现出。它通常比复合的级别低。但是当derived class需要访问protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

和复合不同,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序库开发者而言没可能很重要。

四十:Use multuple inheritance judiciously.明智而审慎地使用多重继承

多重继承可能会导致较多地歧义。

如果出现菱形多重继承。为了防止数据重复,必须采用“virtual继承”

class File {};
class InputFile :virtual public File {};
class OutputFile :virtual public File {};
class IOFile:public InputFile, public OutputFile{};

virtual继承的那些classes所产生的对象往往比non-virtual的体积大,访问会慢一些。

非必要不要使用virtual base classes,尽可能避免在其中放置数据。

//todo

发布了35 篇原创文章 · 获赞 5 · 访问量 408

猜你喜欢

转载自blog.csdn.net/qq_33776188/article/details/103145146