C++进阶_Effective_C++第三版(六) 继承与面向对象设计 Inheritance and Object-Oriented Design

继承与面向对象设计 Inheritance and Object-Oriented Design

面向对象编程已经风靡编程界,关于继承、派生、virtual函数等等需要深入了解。

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

Make sure public inheritance models “is-a”.
令派生类class D以public形式继承基类class B,这便是告诉C++编译器每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。如下例子:

class Person {};
class Student: public Person{};

根据生活经验,每个学生都是人,但并非每个人都是学生。这便恰好对应public继承体系。于是任何函数如果期望获得一个类型为Person的实参,都也可以接受一个Student对象:

void eat(const Person& p);	//任何人都会吃
void study(const Student& p);	//只有学生才到学校学习
Person p;		//p是人
Student s;	//s是学生
eat(p);		//没问题,p是人
eat(s);		//没问题,s是学生,而学生也是is-a人
study(s);		//没问题,s是学生
study(p);		//错误!p不是学生

public继承和is-a之间的等价关系听起来颇为简单,但是有时候直觉可能会误导。例如企鹅是一种鸟,鸟可以飞,但是如果以以下C++代码描述关系:

class Bird{
public:
	virtual void fly(); 
};
class Penguin:public Bird{};

但是这个继承体系说企鹅可以飞,而这却不是事实。由于自然语言的不严谨性,很多时候都会误导。但是在其他的领域也有很多误导,比如基础几何。根据所有的正方形都是矩形,然后有如下实现:

class Rectangle{
public:
	virtual void setHeight(int newHeight);
	virtual void setWidth(int newWidth);
	virtual int height() const;
	virtual int width() const;};
void makeBigger(Rectangle& r)	//用以增加r的面积
{
	int oldHeight = r.height();
	r.setWidth(r.width() + 10);		//为r的宽度加10
	assert(r.height() == oldHeight);  //判断r的高度是否未曾改变
}
class Square: public Rectangle{};
Square s;
assert(s.width() == s.height());	//一定为真
makeBigger(s);				//由于继承s是一个矩形,对其增加面积
assert(s.width() == s.height());	//对所有正方形应该仍然为真,但是这里却不为真了。
  • “public继承”意味着is-a。适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象。

33、避免遮掩继承而来的名称

Avoid hiding inherited names.
当位于一个派生类成员函数内指涉基类内的某物(也许是成员函数、tyepdef、成员变量)时,编译器可以找出我们所指涉的东西,因为派生类继承了声明于基类内的所有东西。实际运作方式是,派生类作用域被嵌套在基类作用域内:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
	virtural void mf2();
	void mf3();
};
class Derived:public Base{
public:
	virtual void mf1();
	void mf4();};
//mf4如下实现:
void Derived::mf4()
{mf2();
}

当编译器看到mf4调用mf2.首先查找local作用域(即mf4覆盖的作用域),没找到任何东西名为mf2,再查找外围作用域(即Derived类覆盖的作用域)还是没找到,于是再往外围移动(继承基类Base类),找到了一个名为mf2的东西,停止查找。如果Base内还没有的话,查找动作继续下去,首先找内涵Base的那个namespace(s)的作用域,最后往global作用域去找。
如下例子,基类重载了mf1和mf3,派生类添加一个新的mf3.

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

以作用域为基础的“名称遮掩规则”,基类内所有名为mf1和mf3的函数都被派生类Derived内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承。

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

这些机制为了防止在程序库或框架内建立新的派生类时附带地从疏远的基类继承重载函数。但是通常会想继承重载函数。实际如果正在使用public继承而又不继承那些重载函数,就违反基类和派生类之间的is-a关系。为了解决此问题可以使用using声明式:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
virtural void mf1(int);
	virtural void mf2();
	void mf3();
void mf3(double);
};
class Derived:public Base{
public:
	using Base::mf1;	//基类内名称为mf1的所有东西在派生类作用域内都可见。
using Base::mf3;	//基类内名称为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

有时候不想继承基类的所有函数,但又想调用某些基类函数,可以使用一个简单的转交函数实现:

class Base{
private:
	int x;
public:
	virtural void mf1() = 0;
	virtural void mf1(int);
};
class Derived:private Base{
public:
	virtural void mf1()		//转交函数
	{	Base::mf1();	 }	//暗自成为inline
};

inline转交函数的另一个用途是为那些不支持using声明式的老旧编译器提供一条新路。

  • 派生类内的名称会遮掩基类内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

34、区分接口继承和实现继承

Differentiate between inheritance of interface and inheritance of implementation.
表面上简单的public继承,经过更严密的检查,其实由两部分组成,函数接口继承和函数实现继承,这两种继承的差别很像函数声明与函数定义之间的差异。
做为class的设计者,有时候希望派生类只继承成员函数的接口,有时候希望派生类同时继承函数的接口和实现,但又希望能够覆写它们所继承的实现,有时候希望派生类同时继承函数的接口和实现,并且不允许覆写任何东西。
如下例子:

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg);
	int objectID() const;};
class Rectangle:public Shape{};
class Ellipse:public Shape{};

Shape是个抽象class:它的pure virtual函数draw使它成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其派生类的实体。尽管如此,Shape哈斯强烈影响了所有以public形式继承它的派生类。 成员函数的接口总会被继承。public继承意味着is-a,所以对基类为真的任何事情一定对其派生类为真。因此如果某个函数可施行于某class身上,一定也可施行于其派生类身上。pure virtual函数有两个突出的特性:必须被任何“继承了它们”的具体class重新声明,而且它们在抽象class中通常没有定义。
声明一个pure virtual函数的目的是为了让派生类只继承函数接口。对于Shape::draw函数很合理,因为所有Shape对象都应该是可绘出的,Shape::draw得声明式是对具体派生类设计的一种约束,必须提供一个draw函数,但不干涉怎么实现它。比较意外的是,可以为pure virtual函数提供定义,但调用它的唯一途径是调用时明确指出其class名称:

Shape* ps = new Shape;			//错误,Shape是抽象的
Shape* ps1 = new Rectangle;		//没问题
ps1->draw();					//调用Rectangle::draw
Shape* ps2 = new Ellipse;		//没问题
ps2->draw();					//调用Ellipse::draw
ps1->Shape::draw();			//调用Shape::draw
ps2->Shape::draw();			//调用Shape::draw

简朴的impure virtual函数和pure virtual函数有点不同。派生类继承其函数接口,但impure virtual函数会提供一份实现代码,派生类可能覆写它。声明简朴的(非纯)impure virtual函数的目的,是让派生类继承该函数的接口和缺省实现:

class Shape{
public:
	virtual void error(const std::string& msg);};

其接口表示,每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉派生类的设计者,必须支持一个error函数,如果不想自己写一个,可以使用Shape类提供的缺省版本。
如果成员函数是个non-virtual函数,意味是它不打算在派生类中有不同的行为。实际上一个non-virtual成员函数所表现的不变性凌驾其特异性,因为它表示不论派生类多么特异性,它的行为都不可能改变。声明non-virtual函数的目的是为了令派生类继承函数的接口及一份强制性实现。可以把Shape::objectID的声明看作是:每个Shape对象都有一个用来产生对象识别码的函数:此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为。由于non-virtual函数代表的意义是不变性凌驾特异性,所以绝不应该在派生类中被重新定义。

  • 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口。
  • pure virtual函数只具体指点接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

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

Consider alternatives to virtual functions.
假设写一个视频游戏软件,打算为游戏内的人物设计一个继承体系。游戏属于暴力砍杀类型,剧中人物被伤害或因其他因素会降低健康状态,提供一个成员函数healthValue,返回一个整数,表示人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,所以此函数声明为virtual,未将其声明为pure virtual,表示会有缺省算法:

class GameCharacter{
public:
	virtual int healthValue() const;	//返回人物的健康指数,派生类可重新定义};

但是有一个流派,主张virtual函数应该几乎总是private。保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:

class GameCharacter{
public:
	int healthValue() const;	//返回人物的健康指数,派生类可重新定义
	{//做一些事前工作
		int retVal = doHealthValue();//做一些事后工作
		return retVal;
	}
private:
	virtual int doHealthValue() const
	{}
};

这种设计就是“令使用者通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一个独特表现形式。把这个non-virtual函数称为virtual函数的外覆器(wrapper)。
NVI手法的一个优点就是保证在virtual函数进行真正工作之前和之后被调用,意味外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥锁、制造运转日志记录项、验证class约束条件、验证函数先决条件等等。“事后工作”可以包括互斥器解除锁定、验证函数的事后条件、再次验证class约束条件等等。如果让使用者直接调用virtual函数,就没有任何好办法可以做这些事。
在NVI手法下其实没必要让virtual函数一定得是private。某些class继承体系要求派生类在virtual函数实现内必须调用其基类的对应兄弟,virtual函数必须是protected,不能是private。有时候virtual甚至一定得是public(例如具备多态性质的基类的析构函数)。
NVI手法对public virtual函数而言是一个替代方案,但从某种设计角度看,实际没多少用,因为还是使用virtual函数计算每个人的健康指数。另一个设计主张是人物健康指数的计算和人物类型无关,这样的计算完全不需要“人物”这个成分。例如要求每个人物的构造函数接受一个指针,指向一个健康计算函数,就可以调用该函数进行实际计算:

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

这个做法是常见的Strategy设计模式的简单应用。提供和一些弹性:
同一个人物类型的不同实体可以有不同的健康计算函数:

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

某已知人物的健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。也就是说健康指数计算函数不再是GameCharacter继承体系内的成员函数,这些计算函数并未特别访问即将被计算健康指数的那个对象的内部成员。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。
但是如果需要non-public信息进行精确计算,此时就需要弱化class的封装,实现以non-mmber函数访问class的non-public成分。例如可声明那个non-mmber函数为friends,huo为其实现某一部分提供public访问函数。
对于如果对于Strategy设计模式得实现不再使用函数指针,而是改用一个类型为tr1::function的对象,这些约束就都不见了。这样的对象可持有任何可调用物(函数指针、函数对象、成员函数指针),只要其签名式兼容于需求端,改用tr1::function实现:

class GameCharacter; 			//前置声明
int defaultHealthCalc(const GameCharacter& gc); 	//计算健康指数的缺省算法
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具现体的目标签名式是“接受一个reference指向const GameCharacter并返回int”。所谓兼容意思这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。
和前一个设计比较,这个设计几乎相同。唯一不同是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针:

short calcHealth(const GameCharacter&);	//健康计算函数 返回non-int
struct HealthCalculator{		//为计算健康而设计的函数对象
	int operator() (const GameCharacter&) const{}
};
class GameLevel{
public:
	float health(const GameCharacter&) const;	//成员函数,返回non-int};
Class EvilBadGuy:public GameCharacter{		//一种人物类型
public:
	explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf)
	{}
};
Class EyeCandyCharacter:public GameCharacter{		//另一种人物类型
public:
	explicit EyeCandyCharacter (HealthCalcFunc hcf = defaultHealthCalc):GameCharacter(hcf)
	{}
};
EvilBadGuy ebg1(calcHealth);			//使用函数
EyeCandyCharacter ecc1(HealthCalculator())		//使用函数对象
GameLevel currentLevel;
…
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel,_1));	//使用成员函数

因为GameLevel类ude成员函数health宣称自己接受一个参数(GameCharacter的引用),但实际接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个,但是GameCharacter的健康计算函数只接受一个参数,所以使用tr1::bind将其转化使得GameLevel::health可被使用。

  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用物。

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

Never redefine an inherited non-virtual function.
如有继承体系class D由class B以public形式派生而来,class B定义一个public成员函数mf:

class B {
public:
	void mf();};
class D: public B {};
D x;
//调用1
B* pB = &x;
pB->mf();
//调用2
D* pD = &x;
pD->mf();

上述两个调用都是调用B:mf(),虽然指针不同,但是指向的是同一个对象。但是如果D定义了自己的mf版本:

class D: public B{
public:
	void mf();	//遮掩了B::mf};
pB->mf();	//调用B::mf
pD->mf();//调用D::mf

导致这个结果是因为non-virtual函数如B::mf和D:mf都是静态绑定。意思是由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生的类”的对象。virtual函数是动态绑定的,所以不受这个问题影响,如果mf是个virtual函数,不论是通过pB或pD调用mf,都会导致调用D::mf,因为pB和pD真正指向的都是一个类型为D的对象。

  • 绝对不要重新定义继承而来的non-virtual函数。

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

Never redefine a function’s inherited default parameter value.
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继承缺省参数值。
//	但若以指针调用此函数,可以不指定参数值,
//	因为动态绑定下这个函数会从其base继承缺省参数值。};
// 以下调用
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

ps、pc和pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型。不论它们真正指向什么,它们的静态类型都是Shape*。对象的所谓动态类型则是指“目前所指对象的类型”。也就是说上代码中pc的动态类型是Circle*,pr的动态类型是Rectangle*。ps没有动态类型,因为它尚未指向任何对象。动态类型可以在程序执行过程中改变:

ps = pc;	//ps的动态类型是Circle*
ps = pr;	//ps的动态类型是Rectangle*
pc->drwa(Shape::Red);	//调用Circle::draw(Shape::Red)
pc->draw(Shape::Red);	//调用Rectangle::draw(Shape::Red)

因为virtual函数是动态绑定,而缺省参数值却是静态绑定,所以调用一个定义于派生类内的virtual函数的同时,却使用基类为它所指定的缺省参数值:
pr->draw(); //调用Rectangle::draw(Shape::Red)

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

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

Model “has-a” or “is-implemented-in-terms-of” through composition.
复合(composition)是类型之间的一种关系,当某种类型的对象内含其他类型的对象便是这种关系。例如:

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

例子中Person对象由string、address、PhoneNumber构成。复合(composition)有很多同义词:layering(分层),containment(内含),aggregation(聚合)和embedding(内嵌)。
复合实际意味着“has-a”(有一个)或is-implemented-in-terms-of(根据某物实现出)。程序中的对象其实相当于所塑造的世界中的某些事物,例如人,汽车,一张张视频画面等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,像缓冲区、互斥锁、查找树等等。这些对象相当于软件的实现域。当复合发生于应用域内的对象之间,表现出has-a的关系,当它发生于实现域则是表现is-implemented-in-terms-of的关系。
比较麻烦的是区分is-a和is-implemented-in-terms-of这两种关系,假设需要一个template,希望制造出一组classes用来表现由不重复对象组成的sets。此时可以使用标准库的list template,让Set template继承于std::list。也就是Set继承list。声明如下:
template //错误的用法,将list应用于Set
class Set:public std::list { … };
因为如果按此实现,Set为派生类,std::list为基类,根据public继承体系定义,对基类为真的事情对派生类也为真。list可以内含重复元素,但是Set不满足,此设计是错误的。由于这两个类不是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的成员函数可以大量依赖list及标准程序库其他部分提供的机能来完成,如下:

template<class T>
bool Set<T>::member(const T& item)const
{
	return std::find(rep.begin(), rep.end(), item) != rep.end();
}
template<class T>
void Set<T>::insert(const T& item)
{
	if(!member(item))
		rep.push_bak(item);
}
template<class 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<class T>
std::size_t Set<T>::size() const
{
	return rep.size();
}
  • 复合(composition)的意义和public继承完全不同。
  • 在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

39、明智而审慎地使用private继承

Use private inheritance judiciously.
如果classes之间的继承关系是private,编译器不会自动将一个派生类对象转换为一个基类对象。由private继承基类而来的所有成员,在派生类中都会变成private属性,不管在基类中是protected或public属性。Private继承意味着implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,用意就是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹只是一种实现技术。private继承意味只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他含义了。private继承再软件设计层面上没有意义,其意义只及于软件实现层面。Private继承和复合功能很相似,所以尽可能的使用复合,必要时才使用private继承。必要时主要指当protected成员和或virtual函数牵扯进来的时候,或者当空间方面的利害关系足以需要private继承时。
例如有类Widgets,需要记录每个成员函数的被调用次数。运行期间周期性的查看统计信息,此时为了复用代码,我们发现类Timer:

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

我们可以重新定义那个virtual函数,让其取出Widget的当时状态。为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但是public继承在此不适当,因为Widget并不是一个Timer。此时必须以private形式继承Timer:

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

但是这个设计看起来很好,实际可以不使用private继承,可以用复合取而代之,只要在Widget内声明一个嵌套式private class,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型的对象在Widget内:

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

上述两种实现看起来都可以实现需求,但是如果Widget拥有派生类,同时想阻止派生类重新定义onTick。这样只能通过复合这种方式实现,因为派生类取不到WidgetTimer对象。

  • Private继承意味implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当派生类需要访问protected
    基类的成员,或需要重新定义继承而来的virtual函数,使用其实很合理的。
  • 和复合不同,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序库开发者而言,可能很重要。

40、明智而审慎地使用多重继承

Use multiple inheritance judiciously.
C++社群对于多重继承的认识分两大基本阵营,一派认为如果单一继承实好的,多重继承一定更好。另一派认为,单一继承是好的,但多重继承不值得使用。
当多重继承进入设计框架,程序有可能从一个以上的基类继承相同名称。那会导致较多的歧义。例如:

class BorrowableItem{
public:
	void checkOut();};
class ElectronicGadget(){
private:
	bool checkOut()const;};
class MP3Player: public BorrowableItem, public ElectronicGadget{};
MP3Player mp;
mp.checkOut();	//这里有歧义,无法知道调用哪个checkOut。

为了解决这个歧义,就需要明确指出要调用哪个基类内的函数:、

mp.BorrowableItem::checkOut();

多重继承中,如果这些基类并不在继承体系中,但又有更高级的基类,这会导致致命的钻石型多重继承:

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

任何时候如果一个继承体系中其中某个基类和某个派生类之间有一条以上的相通路线,这样会导致一个问题,是否打算让基类内的成员变量经由每一条路径被复制,假设File类有个成员变量fileName,那么IOFile从其每一个基类继承一份,所以其对象内应该有两份fileName成员变量。但从另一角度说,简单的逻辑告诉我们,IOFile对象只该有一个文件名称,所以其继承自两个基类而来的fileName不该重复。C++中其缺省做法为执行复制,如果那个不是需要的,必须令那个带有此数据的类成为一个virtual基类。为了实现此必须令所有直接继承它的类采用virtual继承:

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

C++标准程序库内含一个多重继承体系,其结构为钻石型继承体系,不过其类为类模板,名称为basic_ios,basic_istream,basic_ostream和basic_iostream。
从正确行为的观点看,public继承应该总是virtual。但是使用virtual继承的那些类所产生的对象往往比使用non-virtual继承的对象体积大,访问virtual基类成员变量时,也比访问non-virtual基类成员变量速度慢。而且支配“virtual基类初始化”的规则比non-virtual基类的更复杂且不直观。因为virtual基类的初始化责任是由继承体系的最底层类负责,这样类如果派生自virtual基类而需要初始化,其必须认知其virtual基类,当一个新的派生类加入继承体系中,必须承担其virtual基类的初始化责任。所以非必要不是使用virtual基类,必须使用virtual基类,尽可能避免在其中放置数据。

  • 多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual基类不带任何数据,将是最具有实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface•class”和“private继承某个协助实现的class”的两相组合。

上一篇: C++进阶_Effective_C++第三版(五) 实现 Implementations
下一篇: C++进阶_Effective_C++第三版(七) 模板与泛型编程 Templates and Generic Programming

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章