文章目录
前言
这本书看到后面,不多写点注释,第二次看的时候真看不懂写啥!!!为了不用从头理解一次,多写一点理解的东西总是好的。
条款36:绝不重新定义继承而来的 non-virtual 函数
Never redefine an inherited non-virtual function.
问题来源
class D
是由 class B
以 public
形式派生而来,class B
定义有一个 public
成员函数 mf
。函数 mf
参数和返回值不重要,所以假设两者皆为 void
,如下:
class B
{
public:
void mf();
...
};
class D: public B { ... };
虽然我们对 B
, D
和 mf
一无所知,但面对一个类型为 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::mf
和 D::mf
都是静态绑定(statically bound,见条款37)。意思是,由于 pB
被声明为一个 pointer-to-B
,通过 pB
调用的 non-virtual
函数永远是 B
所定义的版本,即使 pB
指向一个类型为 “B
派生之 class
” 的对象,还是调用的 B::mf
。
但如果使用 virtual
函数却是动态绑定(dynamically bound,见条款37),使用它们不会有上面说的问题。
如果 mf
是个 virtual
函数,不论是通过 pB
或 pD
调用 mf
,都会导致调用 D::mf
,因为 pB
和 pD
真正指的都是一个类型为 D
的对象。
如果你正在编写 class D
并重新定义继承自 class B
的 non-virtual
函数 mf
,D
对象很可能很诡异(原文翻译:展现出精神分裂的不一致行径)。
更明确地说,当 mf
被调用,任何一个 D
对象都可能表现出 B
或 D
的行为。因为决定它们表现行为的因素不在对象自身,而在于 “指向该对象的指针” 声明的类型。References
也会展现和指针一样难以理解的行径。
为啥“绝不重新定义继承而来的non-virtual函数”
条款32 已经说过,所谓 public
继承意味is-a (是一种)的关系。
条款34 则描述为什么在 class
内声明一个non-virtual
函数会为该 class
建立起一个不变性(invariant),凌驾其特异性(specialization)。
如果你将这两个观点施行于两个 classes B
和 D
以及 non-virtual
成员函数 B::mf
身上,那么:
- 适用于
B
对象的每一件事,也适用于D
对象,因为每个D
对象都是一个B
对象;(is-a) B
的derived classes
(派生类) 一定会继承mf
的接口和实现,因为mf
是B
的一个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.
前提
本条款说要简化讨论。我们只能继承两种函数:virtual
和 non-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*
这个案例中,ps
,pc
和 pr
都被声明为 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 class
和 Rectangle class
的 draw
声明式各出一半力。
这个事实不只是局限于“ps
,pc
和 pr
都是指针”的情况,即使把指针换成 references
问题仍然存在。重点在于 draw
是个 virtual
函数,而它有个缺省参数值在 derived class
中被重新定义了。。
C++这样做的原因
C++为什么要设计成这样:virtual
函数是动态绑定,缺省参数却是静态绑定。
答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为 virtual
函数决定适当的参数缺省值,这比目前实行的 “在编译器决定” 的机制更 慢 而且更 复杂。 为了程序的执行速度和编译器实现上的简易度,C++做了取舍,结果就是现在所享受的执行效率!
但如果你没有注意本条款所揭示的忠告,很容易发生混淆!
正确地使用此条例
先尝试同时提供缺省参数值给 base
和 derived 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
函数的替代设计,其中之一就是 NVI(non-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
对象由 string
,Address
,PhoneNumber
构成,在程序员之间复合(composition)这个术语有许多同义词,包括layering(分层),containment(内含),aggregation(聚合)和 embedding (内嵌)。
条款32说过,“public继承” 带有 is-a(是一种)的意义,复合也有它自己的意义。实际上它有两个意义:
- 复合意味has-a(有一个)
- 或者 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
- 上述的
Person class
示范 has-a的关系。Person
有一个名称,一个地址,以及语音和传真 两笔电话号码。你不会说“人是一个名称” 或 “人是一个地址”,你会说 “人有一个名称” 和 “人有一个地址”。大多数人接受此一区别毫无困难,所以很少人会对 is-a 和 has-a 感到困惑。 - 比较麻烦的是区分 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
对象并不为真。
由于 Set
和 list
这两个 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
之间的关系,由于Set
和list
之间的关系是本条例的重点,所以我们以教学的清澈度交换 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 Student
以 public
形式继承 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
继承的规则你刚才已经见过了:
- 如果
classes
之间的继承关系是private
,编译器不会自动将一个derived class
对象(例如Student
)转换为一个base class
对象,(例如Person
)。这和public
继承不同,也是为什么通过s
调用eat
会失败的原因。 - 由
private base class
继承而来的所有成员,在derived class
中都会变成private
属性,纵使它们在base class
中原本是protected
或public
属性。
private
继承是一种 implemented-in-terms-of(根据某物实现出)。
- 如果让
class D
以private
形式继承class B
,用意是为了采用class B
内已经备妥的某些特性,不是因为B
对象和D
对象存在有任何观念上的关系。 private
继承纯粹只是一种实现技术(这就是为什么继承自一个private base class
的每样东西在你的class内都是private
:因为它们都只是实现枝节而已)借用条款34提出的术语,
private
继承意味只有实现部分被继承,接口部分应略去。
如果D
以private
形式继承B
,意思是D
对象根据B
对象实现而得,再没有其他意涵了private
继承在软件 “设计” 层面上没有意义,其意义只及于软件实现层面。
Private
继承意味 is-implemented-in-terms-of(根据某物实现出),38条款指出复合的意义也是这样,如果要在两者中选一个:尽可能使用复合,必要时才使用 private
继承。
必要时候指的是:
- 当
protected
成员和/或virtual
函数牵扯进来的时候 - 还有一种激进情况:当空间不足且不需要太考虑时间效率时,可以使用
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
继承,Timer
的 public 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
继承设计。
-
你或许会想设计
Widget
使它得以拥有derived classes
,但同时你可能会想阻止derived classes
重新定义onTick
。 因为如果Widget
继承自Timer
,上面的想法就不可能实现,即使是private
继承也不可能。(条款35曾说derived classes
可以重新定义virtual
函数,即使它们不得调用它,包括private
部分的也可以重新定义)。但如果
WidgetTimer
是Widget
内部的一个private
成员并继承Timer
,Widget
的derived classes
将无法取用WidgetTimer
,因此无法继承它或重新定义它的virtual
函数。(C# 和 JAVA 都有 “阻止derived classes
重新定义virtual
函数” 的能力,JAVA 的final
和 C# 的sealed
),现在你知道怎么在C++中模拟它了。 -
或许你想要将
Widget
的编译依存性降至最低,如果Widget
继承Timer
当Widget
被编译时Timer
的定义必须可见,所以定义Widget
的那个文件必须#include Timer.h
。但如果
WidgetTimer
移除Widget
之外而Widget
内含指针指向一个WidgetTimer
,Widget
可以带着一个简单的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
身上。
现实中的 “empty
” classes
并不真的是 empty
虽然它们从未拥有 non-static
成员变量。却往往内含 typedefs
,enums
,static
成员变量,或 non-virtual
函数。 STL就有许多技术用途的 empty classes
,其中内含有用的成员(通常是 typedefs
),包括 base class
unary_function
和 binary_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
内的checkOut
是 public
, 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
之间有一条以上的想通路线(像上述的 File
和 IOFile
之间有两条路径),分别穿越 (InputFile
和 OutputFile
),你必须面对这样的问题:
- 是否打算让
base class
内的成员变量经由每一条路径被复制? - 假设
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_ios
,basic_istream
,basic_ostream
,basic_iostream
,而非这里的 File
,InputFile
,OutputFile
和 IOFile
。
从正确行为的观点看,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 derived
)class
负责,这暗示
(1) classes
若派生自 virtual bases
而需要初始化,必须认知其 virtual bases
——不论那些 bases
距离多远
(2) 当一个新的 derived class
加入继承体系中,它必须承担其 virtual bases
(不论直接或间接)的初始化责任。
对于 virtual
的忠告
- 非必要不使用
virtual bases
,平常请使用non-virtual
继承 - 如果你必须使用
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
的客户必须以 Iperson
的 pointers
和 references
来编写程序,因为抽象 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
函数 valueDelimOpen
和 valueDelimClose
允许 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
。
由于 valueDelimOpen
和 valueDelimClose
都是 virtual
函数,theName
返回的结果不仅取决于 PersonInfo
也取决于从 PersonInfo
派生下去的 classes
。
现在比如我们要实现 CPerson
,那上面的接留对我们来说是个好消息,因为仔细阅读 IPerson
文档后,你发现 name
和 birthDate
两函数必须返回未经修饰(不带起始符号和结尾符号)的值。
也就是说如果有人名为 Homer,调用其 name
函数理应获得 “Homer” 而不是 “[Homer]” !
CPerson
和 PersonInfo
的关系是,PersonInfo
刚好有若干函数可帮助 CPerson
比较容易实现出来,所以它们的关系是 is-implemented-in-terms-of(根据某物实现出),而我们知道这种关系可以两种技术实现:复合(条款38)和 private
继承(条款39)。
条款39指出复合通常是比较受欢迎的做法,但如果需要重新定义
virtual
函数,那么private
继承是必要的。
本例中CPerson
需要重新定义 valueDelimOpen
和 valueDelimClose
,所以单纯的复合无法应付。最直接的解法就是令 CPerson
以private
形式解成 PersonInfo
,虽然条款39说过,只要多加有点工作,CPerson
也可以结合 “复合+继承” 技术以求有效重新定义 PersonInfo
的 virtual
函数,书里用的是 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
” 的两相组合。