《Effective C++》第三版 第六章 继承与面向对象设计 36~40条例

19 篇文章 0 订阅
4 篇文章 0 订阅

前言

这本书看到后面,不多写点注释,第二次看的时候真看不懂写啥!!!为了不用从头理解一次,多写一点理解的东西总是好的。

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

Never redefine an inherited non-virtual function.
问题来源

class D 是由 class Bpublic 形式派生而来,class B 定义有一个 public 成员函数 mf。函数 mf 参数和返回值不重要,所以假设两者皆为 void,如下:

class B
{
public:
	void mf();
	...
};

class D: public B { ... };

虽然我们对 B, Dmf 一无所知,但面对一个类型为 D 的对象 x

D x;				//x 是一个类型为 D 的对象

如果以下行为:

B* pB = &x;			//获得一个指针指向 x
pB ->mf();			//经由该指针调用 mf

异于以下行为:

D* pD = &x;			//获得一个指针指向 x
pD ->mf();			//经由该指针调用 mf

你可能会很惊讶!(我看到这里也是哈哈)

理论上看起来像是:毕竟两者都通过对象 x 调用成员函数 mf。由于两者所调用的函数都相同,凭借的对象也相同,所以行为也应该相同,对吧?

但事实可能不是如此:如果 mf 是个 non-virtual 函数而 D 定义有自己的 mf 版本,那就不是如此:

class D: public B 
{
public:
	void mf();		//遮掩(hides)了 B::mf; 见条款33
	...
};
pB ->mf();			//调用 B::mf
pD ->mf();			//调用 D::mf

造成这种情况的原因是:non-virtual 函数如 B::mfD::mf 都是静态绑定(statically bound,见条款37)。意思是,由于 pB 被声明为一个 pointer-to-B,通过 pB 调用的 non-virtual 函数永远是 B 所定义的版本,即使 pB 指向一个类型为 “B 派生之 class” 的对象,还是调用的 B::mf

但如果使用 virtual 函数却是动态绑定(dynamically bound,见条款37),使用它们不会有上面说的问题。

如果 mf 是个 virtual 函数,不论是通过 pBpD 调用 mf,都会导致调用 D::mf,因为 pBpD 真正指的都是一个类型为 D 的对象。

如果你正在编写 class D 并重新定义继承自 class Bnon-virtual函数 mfD对象很可能很诡异(原文翻译:展现出精神分裂的不一致行径)。

更明确地说,当 mf 被调用,任何一个 D 对象都可能表现出 BD 的行为。因为决定它们表现行为的因素不在对象自身,而在于 “指向该对象的指针” 声明的类型。References 也会展现和指针一样难以理解的行径。

为啥“绝不重新定义继承而来的non-virtual函数”

条款32 已经说过,所谓 public 继承意味is-a (是一种)的关系。

条款34 则描述为什么在 class 内声明一个non-virtual 函数会为该 class 建立起一个不变性(invariant),凌驾其特异性(specialization)。

如果你将这两个观点施行于两个 classes BD 以及 non-virtual 成员函数 B::mf 身上,那么:

  • 适用于 B 对象的每一件事,也适用于 D 对象,因为每个 D 对象都是一个 B 对象;(is-a
  • Bderived classes(派生类) 一定会继承 mf 的接口和实现,因为 mfB 的一个 non-virtual 函数。

现在,如果 B 重新定义 mf,你的设计便出现矛盾。

如果 D 真有必要实现出与 B 同的mf,并且如果每一个 B 对象——不管多么特化——真的必须使用 B 所提供的 mf 实现码,那么“每个 D 都是一个B” 就不为真。既然如此 D 就不该以 public 形式继承 B

另一方面,如果 D 真的必须以 public 方式继承 B,并且如果 D 真有需要实现出与 B 不同的 mf,那么mf 就无法为 B 反映出 “不变性凌驾特异性” 的性质。

如果要用 public 方式继承,那么 mf 应该声明为 virtual 函数。最后,如果每个 D 真的是一个 B,并且如果 mf 真的为 B 反映出 “不变性凌驾特异性” 的性质,那么 D 便不需要重新定义 mf,而且它也 不应该 尝试这样做。

不论哪一个观点,结论都相同:任何情况下都不该重新定义一个继承而来的 non- virtual 函数。

有一个很容易理解的例子:

条款7:为什么多态性 base classes 内的析构函数应该是 virtual

如果你违反了条款7,也就是你如果在多态性的 base class 内声明一个 non-virtual 析构函数 ,你也违反了本条款,因为 derived classes 绝对不该重新定义一个继承而来的 non-virtual 函数(因为你声明了一个 non-virtual,而如果你没有自己声明一个析构函数,编译器会为你生成一个默认的析构函数!!(条款5说的))。

所以就本质而言,条款7就是本条款的特殊案例,可以重要到成为一个单独的条款。。

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

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

Never redefine a function’s inherited default parameter value.

前提

本条款说要简化讨论。我们只能继承两种函数:virtualnon-virtual 函数。

然而上一个条款告诉我们,重新顶一个继承而来的 non-virtual 函数永远是错误的!!

所以我们可以安全地将本条款的讨论局限于“继承一个带有缺省参数值的 virtual 函数”。

所以这个条款只讨论 virtual 函数哈哈

所以现在这种情况下,本条款成立的理由就非常直接而明确了:virtual 函数是动态绑定(dynamically bound),而缺省参数值却是静态绑定(staticlly bound)。

我们来复习一下 静态绑定动态绑定!!原文中我觉得很有意思的一段描述,作者太皮了:

那是什么意思?你说你那负荷过重的脑袋早已忘记静态绑定和动态绑定之间的差异?(为了正式记录在案,容我再说一次,静态绑定又名前期绑定,early binding;动态绑定又名后期绑定,late binding。)现在让我们来一趟复习之旅吧!

静态类型(static type)

对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型,考虑以下的 class 继承类型:

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* ps;						//静态类型为 Shape*
Shape* pc = new Circle;			//静态类型为 Shape*
Shape* pr = new Rectangle;		//静态类型为 Shape*

这个案例中,pspcpr 都被声明为 pointer-to-Shape 类型,所以它们(指针们)都以它(Shape*)为静态类型,不论它们真正指向什么,它们的静态类型都是 Shape*

动态类型(dynamic type)

对象的所谓动态类型(dynamic type) 则是指“目前所指对象的类型”,也就是说,动态类型可以表现出一个对象将会有什么行为。

以上个例子而言,pc 的动态类型是 Circle*pr 的动态类型是 Rectangle*ps 没有动态类型, 因为它没有指向任何对象。

动态类型,顾名思义,可以在程序执行过程中改变(通常是赋值操作):

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

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

pc -> draw(Shape::Red);			//调用 Circle::draw(Shape::Red)
pr -> draw(Shape::Red);			//调用 Rectangle::draw(Shape::Red)
缺省参数值的 virtual 函数

上面都是我们理解的 virtual 函数,但是考虑缺省参数值的 virtual 函数时,花样来了,C++ 有自己的想法:(像作者说的)virtual函数是动态绑定,而缺省参数却是静态绑定。意思是你可能会在“调用一个定义于 derived class内的 virtual函数” 的同时,却使用 base class 为它所指定的缺省参数值:

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

这个案例说明,pr的动态类型是 Rectangle*,所以调用的是 Rectangle::draw 函数的缺省参数值应该是 GREEN,但是由于 pr 的静态类型是 Shape*,所以这个调用的缺省参数值来自 Shape class 而非 Rectangle class!

不用往上找,已经帮你们移下来了:

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

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

所以结局是这个函数调用有着奇怪并且 几乎绝对没人预料得到 的组合,由 Shape classRectangle classdraw 声明式各出一半力。

这个事实不只是局限于“pspcpr 都是指针”的情况,即使把指针换成 references 问题仍然存在。重点在于 draw 是个 virtual 函数,而它有个缺省参数值在 derived class 中被重新定义了。。

C++这样做的原因

C++为什么要设计成这样:virtual函数是动态绑定,缺省参数却是静态绑定。

答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为 virtual 函数决定适当的参数缺省值,这比目前实行的 “在编译器决定” 的机制更 而且更 复杂。 为了程序的执行速度和编译器实现上的简易度,C++做了取舍,结果就是现在所享受的执行效率!

但如果你没有注意本条款所揭示的忠告,很容易发生混淆!

正确地使用此条例

先尝试同时提供缺省参数值给 basederived classes 的用户:

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 = Red) const;
	...
};

(一开始没看懂为啥有这么奇葩的写法,果然这本书就是经常有奇奇怪怪的写法,然后告诉我们 这样写是错误的!)

这样写很明显有一个问题,代码重复,并且如果按照这种方式,不止重复代码还带着相依性(with dependencies):如果 Shape 内的缺省参数值改变了,所有“重复给定缺省参数值”的那些 derived classes也必须改变,否则它们最终会导致 “重复定义一个继承而来的缺省参数值”。(实在是太奇怪了)

当你想令 virtual 函数表现出你想要的行为但却一直遭遇麻烦时,之前的条例其实告诉我们很好的解决方案:考虑替代设计。

条例35列举不少关于 virtual 函数的替代设计,其中之一就是 NVInon-virtual interface)手法:令 base class内的一个 public non-virtual 函数调用 private virtual 函数,后者可被 derived classes 重新定义。这里我们可以让 non-virtual 函数指定缺省函数,而 private virtual 函数负责真正的工作:

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;	//注意,不须指定缺省参数值
	...
};

由于 non-virtual 函数应该绝对不被 derived classes 覆写(见条款36),按这个设计很清楚就可以让 draw 函数的 color 缺省参数值总是为 Red

请记住
  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数 —— 你唯一应该覆写的东西 —— 却是动态绑定。

该改写的就是 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;			//合成成分物(composed object)
	Address address;			//同上
	PhoneNumber voiceNumber;	//同上
	PhoneNumber faxNumber;		//同上
};

本例之中 Person 对象由 stringAddressPhoneNumber 构成,在程序员之间复合(composition)这个术语有许多同义词,包括layering(分层),containment(内含),aggregation(聚合)和 embedding (内嵌)。

条款32说过,“public继承” 带有 is-a(是一种)的意义,复合也有它自己的意义。实际上它有两个意义:

  1. 复合意味has-a(有一个)
  2. 或者 is-implemented-in-terms-of(根据某物实现出)

这两个意义可以说是在咱们软件中,处理两个不同的领域。

  • 程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车、一张张视频画面等等,像这种对象属于应用域部分。
  • 其他对象则纯粹是实现细节上的人工制品,像是缓冲区(buffers)、互斥器(mutexes)、查找树(search trees)等等,这些对象相当于你的软件的实现域。

当复合发生于应用域的对象之间,表现出 has-a的关系;当它发生于实现域内则是表现 is-implemented-in-terms-of 的关系。

如何区 is-a 和 has-a 、is-implemented-in-terms-of
  1. 上述的 Person class 示范 has-a的关系。Person 有一个名称,一个地址,以及语音和传真 两笔电话号码。你不会说“人是一个名称” 或 “人是一个地址”,你会说 “人有一个名称” 和 “人有一个地址”。大多数人接受此一区别毫无困难,所以很少人会对 is-ahas-a 感到困惑。
  2. 比较麻烦的是区分 is-a(是一种) 和 is-implemented-in-terms-of(根据某物实现出)这两种对象关系。
区分 is-a 和 is-implemented-in-terms-of 的案例

假设你需要一个 template 希望制造出一组 classes 用来表现由不重复对象组成的 sets 由于复用(reuse)是件很爽的事(程序员的本质就是学会复用代码),所以我们可以站在巨人的肩膀上,比如说采用标准程序库提供的 set template 是的。如果他人写的 template 合乎需求,我们何必再写一个?(还不一定有别人写好呢。。)

不幸的是告诉你吧,set的实现往往招致“每个元素耗用三个指针” 的额外开销。因为 sets通常以 平衡查找树balanced search trees)实现而成,使它们在查找、安插、移除元素时保证拥有对数时间效率。当速度比空间重要时,你使用 set 设计是没毛病的。

但是如果你程序的空间比速度更重要呢? 那么标准库的 set 提供给你的是个错误决定下的取舍,似乎这种情况下你还是要自己写个 template。。

复用啊

但是,作者也说到了 “复用是件美好的事”,(我也觉得复制粘贴或者直接使用已有的接口不是更爽吗?)。如果你是一位数据结构专家,你就会知道,实现 set 的方法太多了,其中一种就是在底层采用 linked lists 而你刚好知道,标准程序库有一个 list template ,所以我们可以复用它。

简单说,我们使用自己设计的新Set template 继承 std::list ,也就是让 Set<T> 继承 list<T>。对于现在的思路来说,Set 对象就是个 list 对象,于是声明的Set template 如:

template<typename T>						//将list应用于 Set. 错误做法!!
class Set: public std::list<T> { ... };

这样做表面上看还可以,但实际一点都不好,还糟糕透了!! 还记得条款32说过:如果 D 是一种 B ,对 B 为真的每一件事对 D 也为真。 但 list 可以存储重复相同的元素,如果数值 3051 被安插到 list<int> 两次,那 list 将包含两笔3051,Set不可以内含重复元素,如果数值3051被安插到Set<int>两次,这个 set 理论上只能包含一笔 3051。因此 “Set 是一种 list” 并不为真,因为对 list 对象为真的某些事情对 Set 对象并不为真。

由于 Setlist 这两个 classes 之间并非 is-a的关系,所以 public 继承不适合用来塑模它们。正确的做法是,你应当了解,Set 对象 可根据一个 list 对象实现出来

template<class T>					//将 list 应用于 Set 正确做法
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 及标准程序库其他部分提供的机能来完成,所以其实现很直观也很简单,只要你熟悉 STL 编写程序:

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);	//条款 42对 “typename” 的讨论
	if (it != rep.end()) rep.erase(it);
}

template<typename T>
std::size_T Set<T>::size() const
{
	return rep.size();
}

这些函数都很简单,都可以作为 inline 函数。但是写 inline 函数之前先看看条款30。

说明一下 Set 接口遵循 STL 容器的协议,更符合条款18接口设计的警告:“让它更容易被正确使用,不易被误用”。但是这如果要遵循哪些协议,就需要为Set添加许多东西,那将模糊了它和 list 之间的关系,由于 Setlist 之间的关系是本条例的重点,所以我们以教学的清澈度交换 STL 兼容性。
以上作者原话

总结就是:Set接口和 list 间的关系并非是 is-a,而是 is-implemented-in-terms-of,这个关系是值得我们关注和理解的,不会因为条款18,这层关系就不成立
(总之为了理解 is-implemented-in-terms-of

请记住
  • 复合(composition)的意义和 public 继承完全不同
  • 在应用域(application domain),复合意味 has-a(有一个)。在实现域(implementation domain),复合意味 is-implemented-in-terms-of (根据某物实现出)。

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

Use private inheritance judiciously

话题引出

条款32曾经论证过C++如何将 public 继承视为 is-a 关系。在那个例子中我们有个继承体系,其中 class Studentpublic 形式继承 class Person,于是编译器在必要时刻(为了让函数调用成功)将 Students 暗自转换为 Persons。现在我再重复该例的一部分,并以 private 继承替换 public 继承:

class Person { ... };
class Student: private Person { ... };		//这次改用private继承
void eat (const Person& p);					//任何人都会吃
void study (const Student& s);				//只有学生才在校学习.

Person P;									//p是人
Student s;									//s是学生

eat (p);									//没问题,p是人,会吃。
eat(s);										//错误!吓,难道学生不是人?!

显然 private 继承并不意味 is-a 关系。我们来认识一下这个继承关系。

private 继承

private 继承的规则你刚才已经见过了:

  1. 如果 classes 之间的继承关系是 private ,编译器不会自动将一个 derived class 对象(例如 Student)转换为一个 base class 对象,(例如 Person)。这和 public 继承不同,也是为什么通过 s 调用 eat 会失败的原因。
  2. private base class 继承而来的所有成员,在 derived class 中都会变成 private 属性,纵使它们在 base class 中原本是 protectedpublic 属性。

private 继承是一种 implemented-in-terms-of(根据某物实现出)。

  • 如果让 class Dprivate 形式继承 class B,用意是为了采用 class B 内已经备妥的某些特性,不是因为 B 对象和 D 对象存在有任何观念上的关系。
  • private 继承纯粹只是一种实现技术(这就是为什么继承自一个private base class 的每样东西在你的class内都是 private:因为它们都只是实现枝节而已)

    借用条款34提出的术语,private 继承意味只有实现部分被继承,接口部分应略去。
    如果 Dprivate 形式继承 B,意思是D 对象根据 B 对象实现而得,再没有其他意涵了

  • private 继承在软件 “设计” 层面上没有意义,其意义只及于软件实现层面。

Private 继承意味 is-implemented-in-terms-of(根据某物实现出),38条款指出复合的意义也是这样,如果要在两者中选一个:尽可能使用复合,必要时才使用 private 继承。

必要时候指的是:

  1. protected 成员和/或 virtual 函数牵扯进来的时候
  2. 还有一种激进情况:当空间不足且不需要太考虑时间效率时,可以使用 private 继承,这是激进的情况!

我们看案例就明白了!

举例说明
需求

假设我们的程序涉及 Widgets,而我们决定应该较好地了解如何使用 Widgets,例如我们不只想要知道 Widget 成员函数多么频繁地被调用,也想知道经过一段时间后调用比例如何变化。

带有多个执行阶段(execution phases)的程序,可能在不同阶段拥有不同的行为轮廓(behavioral profiles)。
例如编译器在解析(parsing)阶段所使用的函数,大大不同于在最优化(optimization)和代码生成(code generation)阶段所使用的函数

我们决定修改 Widget class 让它记录每个成员函数的被调用次数。运行期间,我们将周期性地审查那份信息,也许再加上每个 Widget 的值,以及我们需要评估的任何其他数据。 为了实现这个功能,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了。

实现

我们尽可能复用现有的代码,尽量少写新代码。如果你有自己写的工具库,那就更好了,作者说:在自己的工具百宝箱中翻箱倒柜,并且很开心地发现了这个 class

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

这就是我们需要的定时器,一个 Timer 对象,可调整为以我们需要的任何频率滴答前进,每次滴答就调用某个 virtual 函数。我们可以重新定义那个 virtual 函数,让后者取出 Widget 的当时状态。完美!

为了让 Widget 重新定义 Timer 内的 virtual 函数,Widget 必须继承自 Timer。但 public 继承在此比例并不恰当,因为 Widget 并不是个 Timer

毕竟,Widget 的客户总不该能够对着一个 Widget 调用 onTick 吧,因为观念上那并不是 Widget 接口的一部分
如果允许那样的调用动作,很容易造成客户不正确地使用 Widget 接口,那会违反条款18的忠告:“让接口容易被正确使用,不易被误用”

所以在这里,public 继承不是一个好策略。

我们必须以 private 形式继承 Timer

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

private继承,Timerpublic onTick 函数在 Widget 内变成 private,而我们重新声明(定义)时仍然能把它留在那(留在Widget内并且可以重新定义,还不会被客户误用)。如果我们把 onTick 放进 public 接口内会误导客户端以为他们可以调用这个接口。

用复合代替 private 继承

这是个好设计!但不值钱也不一定值得提倡,因为 private 继承并非绝对必要。如果我们决定以复合(composition)取而代之,是可以的。只要在 Widget 内声明一个嵌套式 private class,后者以 public 形式继承 Timer 并重新定义 onTick ,然后放一个这种类型的对象于 Widget 内:

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

在这里插入图片描述
这个设计比只使用 private 继承要复杂一些,因为它同时涉及 public 继承和复合,并导入一个新 class(WidgetTimer)

这样做的原因是,作者主要为了提醒你:解决一个设计问题的方法不只一种,而训练自己思考多种做法是值得的(见条款35)。尽管如此,起码有两个理由,告诉你应该选择这样的 public 继承加复合,而不是选择原先的 private 继承设计。

  1. 你或许会想设计 Widget 使它得以拥有 derived classes,但同时你可能会想阻止 derived classes 重新定义 onTick。 因为如果 Widget 继承自 Timer,上面的想法就不可能实现,即使是 private 继承也不可能。(条款35曾说 derived classes 可以重新定义 virtual 函数,即使它们不得调用它,包括 private 部分的也可以重新定义)。

    但如果 WidgetTimerWidget 内部的一个 private成员并继承 TimerWidgetderived classes 将无法取用 WidgetTimer,因此无法继承它或重新定义它的 virtual 函数。(C# 和 JAVA 都有 “阻止 derived classes 重新定义 virtual 函数” 的能力,JAVA 的 final 和 C# 的 sealed),现在你知道怎么在C++中模拟它了。

  2. 或许你想要将 Widget 的编译依存性降至最低,如果Widget 继承 TimerWidget 被编译时 Timer 的定义必须可见,所以定义 Widget 的那个文件必须 #include Timer.h

    但如果 WidgetTimer 移除 Widget 之外而 Widget 内含指针指向一个 WidgetTimerWidget 可以带着一个简单的 WidgetTimer 声明式,不需要再 #include 任何与 Timer 有关的东西。对大系统而言,如此的解耦(decouplings)可能是重要的措施。(条款31:编译依存性的最小化)

使用 private 激进的情况

private继承主要用于 “当一个意欲成为 derived class 者想访问一个意欲成为 base class 者的 protected 成分,或为了重新定义一或多个 virtual 函数”。但这时两个 classes 之间的概念关系其实是is-implemented-terms-of(根据某物实现出)而非is-a

然而作者也提到一种激进的情况涉及空间最优化,可能会促使你选择 “private 继承” 而不是 “继承 + 复合”。

  • 这种激进情况,只适用于你所处理的 class 不带任何数据时,这样的class 没有 non-static 成员变量,没有 virtual 函数(因为这种函数的存在,会为每个对象带来一个 vptr,条款7说到),也没有 virtual base classes(因为这样的 base class 也会招致体积上的额外开销,条款40说到)。

  • 于是这种所谓的 empty classes 对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++ 裁定凡事独立(非附属)对象都必须有非零大小,所以如果你这样做:

    class Empty {	};			//没有数据,所以其对象应该不使用任何内存
    
    class HoldsAnInt			//应该只需要一个 int 空间
    {
    private:
    	int x;
    	Empty e;				//应该不需要任何内存
    };
    

在这个案例中,你会发现 sizeof(HoldsAnInt) > sizeof(int);,一个空的类 Empty 成员变量竟然要求内存,在大多数编译器中 sizeof(Empty) 获得 1,因为面对“大小为零之独立(非附属)对象”,通常C++ 官方勒令默默安插一个 char 到空对象内。然而齐位需求(alignment,见条款50)可能造成编译器为类似 HoldsAnInt 这样的 class 加上一些衬垫(padding),所以有可能 HoldsAnInt 对象不只获得一个 char 大小,也许实际上被放大到足够又存放一个 int。不同的编译器有不同情况发生。

但其实“独立(非附属)”对象的大小一定不为零。也就是说,这个约束不适用于 derived class 对象内的 base class 成分,因为它们并非独立(非附属)。如果你继承 Empty,而不是内含一个那种类型的对象:

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

几乎可以确定 sizeof(HoldsAnInt) == sizeof(int),这是所谓的 EBO(empty base optimization;空白基类最优化),我试过所有编译器都有这样的结果。如果你是一个程序库开发人员,而你的客户非常在意空间,那么值得注意 EBO,另外 EBO 一般只在单一继承(而非多重继承)下才可行,统治 C++ 对象布局的那些规则通常表示 EBO 无法被施行于 “拥有多个 base” 的 derived classes 身上。

现实中的 “emptyclasses 并不真的是 empty 虽然它们从未拥有 non-static 成员变量。却往往内含 typedefsenumsstatic成员变量,或 non-virtual 函数。 STL就有许多技术用途的 empty classes,其中内含有用的成员(通常是 typedefs),包括 base class unary_functionbinary_function 这些是“用户自定义之函数对象”,通常会继承的 classes

感谢 EBO 的广泛实践,使这样的继承很少增加 derived classes 的大小。

总结

尽管如此,让我们回到根本,大多数 classes 并非 empty,所以 EBO 很少成为 private 继承的正当理由。进一步说,大多数继承相当于 is-a,这是指 public 继承,不是 private 继承。复合 和 private 继承都意味着 is-implemented-in-terms-of ,但复合比较容易理解,所以无论什么时候,只要可以,你还是选择复合。

当你面对“并不存在 is-a 关系” 的两个 classes,其中一个需要访问另一个的 protected 成员,或需要重新定义其一或多个 virtual 函数,private 继承极有可能成为最好的设计策略。即便如此,你也看到了,尽管一个混合了 public 继承和复合的设计有较大的复杂度,但往往能够解释出你要的行为。“明智而审慎地使用 private 继承” 意味,在考虑所有其他方案之后,如果仍然认为 private继承是“表现程序内两个 classes 之间的 关系” 的最佳办法,才用它。

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

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

Use multiple inheritance judiciously.

一旦涉及多重继承(multiple inheritance; MI),C++ 社群便分为两个基本阵营。其中之一认为如果单一继承(single inheritance; SI)是好的,多重继承一定更好。另一派阵营则主张,单一继承是好的,但多重继承不值得拥有(或使用)。

本条款的主要目的是带领大家了解多重继承的两个观点。

多重继承

最先需要认清的一件事是,当 MI 进入设计景框,程序有可能从一个以上的 base classes 继承相同名称(如函数、typedef 等等)。那会导致较多的歧义(ambiguity)机会。例如:

class BorrowableItem			//图书挂允许你借某些东西
{
public:
	void checkOut();			//离开时进行检查
	...
};

class ElectronicGadget
{
private:
	bool checkOut() const;		//执行自我检测,返回是否测试成功
	...
};

class MP3Player:public BorrowableItem, public ElectronicGadget	//注意合理的多重继承(某些图书馆愿意借出 MP3 播放器)
{	...	};						//这里,class 的定义不是我们关心的重点

MP3Player mp;
mop.checkOut();					//歧义!调用的是哪个 checkOut?

注意此例之中对 checkOut 的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(BorrowableItem 内的checkOutpublic, ElectronicGadget内的却是private)。这与 C++ 用来解析(resolving)重载函数调用的规则相符:
在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配函数后才检验其可取用性。本例的两个 checkOuts 有相同的匹配程度(译注:因此才造成歧义),没有所谓最佳匹配。因ElectronicGadget : : checkOut的可取用性也就从未被编译器审查。

为了解决这个歧义,你必须明白指出你要调用哪一个base class内的函数:

mp.BorrowableItem::checkOut( ) ;//哎呀,原来是这个checkOut. ..

你当然也可以尝试明确调用 ElectronicGadget: :checkOut,但然后你会获得一个“尝试调用private成员函数”的错误。

多重继承的意思是继承一个以上的base classes,但这些 base classes 并不常在继承体系中又有更高级的 base classes,因为那会导致要命的“钻石型多重继承”:

class File{ ... };
class InputFile: public File { ..... };
class OutputFile: piblic Rile{ ..... };
class IOFile: public InputFile, public OutputFile
{ ... };

在这里插入图片描述
任何时候如果你有一个继承体系,而其中某个 base class 和某个 derived class 之间有一条以上的想通路线(像上述的 FileIOFile 之间有两条路径),分别穿越 (InputFileOutputFile),你必须面对这样的问题:

  1. 是否打算让 base class 内的成员变量经由每一条路径被复制?
  2. 假设File class 有个成员变量 fileName 那么 IOFile 内该有多少笔这个名称的数据呢?

从某个角度说,IOFile 从其每一个 base class 继承一份,所以其对象内应该有两份 fileName 成员变量。但从从另一个角度说,简单的逻辑告诉我们,IOFile 对象只该有一个文件名称,所以它继承自两个 base classes 而来的 fileName 不该重复。

virtual 继承

C++在上面两个问题中没有倾斜立场:两个方案它都支持——虽然缺省做法是执行复制(上面的第一种说法,两份 fileName 成员变量)。如果那不是你想要的,那你必须令那个带有此数据(fileName)的 class (也就是 File)成为一个 virtual base class。为了能够这样做,你必须令所有直接继承自它的 classes 采用 “virtual” 继承:

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

在这里插入图片描述
C++标准库内含一个多重继承体系,其结构就如右图那样,只不过其 classes 其实是 class templates,名称分别是 basic_iosbasic_istreambasic_ostreambasic_iostream,而非这里的 FileInputFileOutputFileIOFile

从正确行为的观点看,public 继承应该总是virtual。 如果这是唯一一 个观点,规则很简单:任何时候当你使用 public 继承,请改用 virtual public 继承。但是正确性并不是唯一观点。为避免继承得来的成员变量重复,编译器必须提供若干幕后戏法(原文是这样说的,我感觉可以理解成:很多操作),而其后果是:

  • 使用 virtual 继承的那些 classes 所产生的对象往往比使用 non-virtual 继承的兄弟们体积大,访问 virtual base classes 的成员变量时,也比访问 non-virtual base classes 的成员变量速度慢。
  • 种种细节因编译器不同而异,但基本重点很清楚:你得为 virtual 继承付出代价。

virtual 继承的成本还包括其他方面。支配“virtual base classes 初始化”的规则比起 non-virtual bases 的情况远为复杂且不直观。virtual base 的初始化责任是由继承体系中的最低层(most derivedclass 负责,这暗示
(1) classes 若派生自 virtual bases 而需要初始化,必须认知其 virtual bases——不论那些 bases 距离多远
(2) 当一个新的 derived class 加入继承体系中,它必须承担其 virtual bases (不论直接或间接)的初始化责任。

对于 virtual 的忠告
  1. 非必要不使用 virtual bases,平常请使用 non-virtual 继承
  2. 如果你必须使用 virtual base classes 尽可能避免在其中放置数据,这样你就不用担心这些 classes 身上的初始化(和赋值)所带来的诡异事情了。

Java 和 .NET 的 Interfaces (接口)值得注意,它在许多方面兼容于 C++ 的 virtual base class而且也不允许含有任何数据

C++ Interface class 案例

塑膜“人”的C++ Interface class

class IPerson
{
public:
	virtual ~IPerson();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
};

IPerason 的客户必须以 Ipersonpointersreferences 来编写程序,因为抽象 classes 无法被实体化创建对象。为了创建一些可被当作 IPerson 来使用的对象, IPerson 的客户使用 factory functions(工厂函数) 将 “派生自 IPerson 的具象 class ” 实体化:

//factory function (工厂函数) , 根据一个独一无二的数据库 ID 创建一个 Person 对象。
//条款18告诉你 为什么返回类型不是原始指针
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

//这个函数从使用者手上取得一个数据库ID
DatabaseID askUserForDatabaseID();

DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
...												//创建一个对象支持 Iperson 接口 籍由 Iperson 成员函数处理 *pp

但是 makePerson 如何创建对象并返回一个指针指向它呢?无疑地一定有某些派生自 IPerson 的具象 class,在其中 makePerson 可以创建对象。

假设这个 class 名为 CPerson。就像具象 class 一样,CPerson 必须提供“继承自 IPerson” 的 pure virtual 函数的实现代码。我们可以从无到有写出这些东西,但更好的是 利用既有组件,后者做了大部分或所有必要事情

例如,假设有个既有的数据库相关 class,名为 PersonInfo,提供 CPerson 所需要的实质东西:

class PersonInfo
{
public:
	explicit PersonInfo(DatabaseID pid);
	virtual ~PersonInfo();
	virtual const char* theName() const;
	virtual const char* theBirthDate() const;
	...
private:
	virtual const char* valueDelimOpen() const;		//详下
	virtual const char* valueDelimClose() const;
	...
};

可能你看见 const char* 时会 觉得它是个旧式的 class (C表达C++中的 string 对象),但是如果这个函数的结果让人满意,为啥不用它?(原文:如果鞋子合脚,干嘛不穿它?)

显而易见,PersonInfo 被设计用来协助以各种格式打印数据库字段,每个字段值的起始点和结束点以特殊字符串为界。缺省的头尾界限符号是方括号(中括号),例如字段 “Ring-tailed Lemur” 将被格式化为:

[Ring-tailed Lemur]

但由于方括号不一定是每个人都喜欢用的界限符号,所以两个 virtual 函数 valueDelimOpenvalueDelimClose 允许 derived classes 设定它们自己的头尾界限符号。PersonInfo 成员函数将调用这些 virtual 函数,把适当的界限符号添加到它们的返回值上。以 PersonInfo::theName 为例,代码看起来像这样:

const char* PersonInfo::valueDelimOpen() const
{
	return "[";			//缺省的起始符号
}

const char* PersonInfo::valueDelimClose() const
{
	return "]";			//缺省的结尾符号
}

const char* PersonInfo::theName() const
{
	//保留缓冲区给返回值使用;由于缓冲区是 staticm,因此会被自动初始化为 “全部是0”
	static char value[Max_Formatted_Field_Value_Length];
	
	//写入起始符号
	std::strcpy(value, valueDelimOpen());

	//现在,将 value 内的字符串添付到这个对象的 name 成员变量中(小心,避免缓冲区超限)
	
	//写入结尾符号
	std::strcat(value, valueDelimClose());
	return value;
}

可以先忽略 PersonInfo::theName 的老旧设计(特别是它竟然使用固定大小的 static 缓冲区,那将充斥超限问题和线程问题)

我们把焦点摆到:theName 调用 valueDelimOpen 产生字符串起始符号,然后产生 name 值,然后调用 valueDelimClose

由于 valueDelimOpenvalueDelimClose 都是 virtual 函数,theName 返回的结果不仅取决于 PersonInfo 也取决于从 PersonInfo 派生下去的 classes

现在比如我们要实现 CPerson ,那上面的接留对我们来说是个好消息,因为仔细阅读 IPerson 文档后,你发现 namebirthDate 两函数必须返回未经修饰(不带起始符号和结尾符号)的值。

也就是说如果有人名为 Homer,调用其 name 函数理应获得 “Homer” 而不是 “[Homer]” !

CPersonPersonInfo 的关系是,PersonInfo 刚好有若干函数可帮助 CPerson 比较容易实现出来,所以它们的关系是 is-implemented-in-terms-of(根据某物实现出),而我们知道这种关系可以两种技术实现:复合(条款38)和 private继承(条款39)。

条款39指出复合通常是比较受欢迎的做法,但如果需要重新定义 virtual 函数,那么 private 继承是必要的。

本例中CPerson 需要重新定义 valueDelimOpenvalueDelimClose,所以单纯的复合无法应付。最直接的解法就是令 CPersonprivate 形式解成 PersonInfo,虽然条款39说过,只要多加有点工作,CPerson 也可以结合 “复合+继承” 技术以求有效重新定义 PersonInfovirtual 函数,书里用的是 private 继承。

CPerson 也必须实现 IPerson 接口,那需得以 public 继承才能完成。这导致多重继承的一个通情达理的应用:将 “public 继承自某接口” 和 “private 继承自某实现” 结合在一起:

class IPerson									//这个class指出需实现的接口
{
public:
	virtual ~IPerson();
	virtual std::string name() const = 0;
	virtual std::string birthDate() const = 0;
};

class DatabaseID { ... };						//稍后被使用 细节不重要

class PersonInfo								//这个class有若干有用函数 可用以实现 IPerson 接口
{
public:
	explicit PersonInfo (DatabaseID pid);		
	virtual ~PersonInfo();
	virtual const char* theName() const;
	virtual const char* theBirthDate() const;
	virtual const char* valueDelimOpen() const;
	virtual const char* valueDelimClose() const;
};

class CPerson: public IPerson, private PersonInfo		//注意!多重继承
{
public:
	explicit CPerson(DatabaseID pid): PersonInfo(pid) { }
	virtual std::string name() const					//实现必要的IPerson成员函数
	{	return PersonInfo::theName();	}

	virtual std::string birthDate() const				//实现必要的IPerson成员函数
	{	return PersonInfo::theBirthDate();	}
private:												//重新定义继承而来的 virtual "界限函数"
	const char* valueDelimOpen() const {	return "";	}
	const char* valueDelimClose() const {	return "";	}
};

UML 图中这个设计看起来像这样:
在这里插入图片描述
这个例子告诉我们,多重继承也有它的合理用途。

故事结束前,请容我说,多重继承只是面向对象工具箱里的一个工具而已。和单一继承比较,它通常比较复杂,使用上也比较难以理解,所以如果你有个单一继承的设计方案,而它大约等价于一个多重继承设计方案,那么单一继承设计方案几乎一定比较受欢迎。

如果你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想——几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候的确是完成任务之 最简洁、最易维护、最合理 的做法,果真如此就别害怕使用它。只要确定,你的确是在明智而审慎 的情况下使用它。

请记住,
  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需要。
  • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果 vitual base classes 不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及 “public 继承某个 Interface class” 和 “private 继承某个协助实现的class” 的两相组合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
有人说c++程序员可以分成两类,读过effective c++的和没读过的。世界顶级c++大师scott meyers成名之作的第三版的确当得起这样的评价。当您读过这本书之后,就获得了迅速提升自己c++功力的一个契机。.(2-1)   在国际上,本书所引起的反响,波及整个计算技术出版领域,余音至今未绝。几乎在所有c++书籍的推荐名单上,本书都会位于前三名。作者高超的技术把握力、独特的视角。诙谐轻松的写作风格、独具匠心的内容组织,都受到极大的推崇和仿效。这种奇特的现象,只能解释为人们对这本书衷心的赞美和推祟。《effective c++》前两个版本的确抓住了全世界无数程序员的目光。原因十分显明:scott meyers 极富实践意义的c++ 研讨方式,描述出专家用以产出干净、正确、高效代码的经验法则和行事法则——也就是他们几乎总是做或不做的某些事。   这本书不是读完一遍就可以束之高阁的快餐读物,也不是用以解决手边问题的参考手册,而是需要您去反复阅读体会的,c++是真正程序员的语言,背后有着精深的思想与无与伦比的表达能力,这使得它具有类似宗教般的魅力。希望这本书自瞄帮助您跨越c抖的重重险阻,领略高处才有的壮美风光,做—个成功而快乐的c++程序员。...      本书一共组织 55 个准则,每一条准则描述一个编写出更好的 c++ 的方式。每一个条款的背后都有具体范例支撑。第三版有一半以上的篇幅是崭新内容,包括讨论资源管理和模板(templates)运用的两个新章。为反映出现代设计考虑,对第二版论题做了广泛的修订,包括异常(exceptions)、设计模式(design patterns)和多线程(multithreading)。      《effective c++》的重要特征包括:    * 高效的 classes、functions、templates 和inheritance hierarchies(继承体系)方面的专家级指导。    * 崭新的 "tr1" 标准程序库功能应用,以及与既有标准程序库组件的比较。    * 洞察 c++和其他语言(例如java、c#、c)之间的不同。此举有助于那些来自其他语言阵营的开发人员消化吸收 c++ 式的各种解法。(2-1)

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值