[C++] public和private继承
1. public继承
以C++进行面向对象编程,最重要的一个规则是:
public inheritance(公开继承)意味着“is-a”(是一种)的关系。
如果你令class D以public形式继承class B,
你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。
你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。
你主张“B对象可派上用场的任何地方,D对象一样可以派上用场(Liskov Substitution Principle)”,
因为每一个D对象都是一种(是一个)B对象。
反之,如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。
C++对于“public继承”严格奉行上述见解。考虑以下例子:
class Person { ... };
class Student: public Person { ... };
根据生活经验我们知道,每个学生都是人,但并非每个人都是学生,这便是这个继承体系的主张。
我们预期,对人可以成立的每一件事,对学生也都成立。
但我们并不预期对学生可成立的每一件事,对人也成立。
人的概念比学生更一般化,学生是人的一种特殊形式。
于是,承上所述,在C++领域中,任何函数如果期望获得一个类型为Person(或pointer-to-Person或reference-to-Person)的实参,
都也愿意接受一个Student对象(或pointer-to-Student或reference-to-Student)。
这个论点只对public继承才成立,
只有当Student以public形式继承Person,C++的行为才会如我所描述。
private继承的意义与此完全不同。
is-a并非唯一存在于class之间的关系,令两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。
将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见。
所以你应该确定你确实了解这些个“class相互关系”之间的差异性,并知道如何在C++中最好的塑造它们。
2. private继承
我们论证了C++如何将public继承视为is-a关系,
在那个例子中我们有个继承体系,其中class Student以public形式继承class Person,
于是编译器在必要时刻(为了让函数调用成功),将Student暗自转换成Person。
现在我再重复该例的一部分,并以private继承替换public继承。
class Person { ... };
class Student: private Person { ... };
在我们探讨其意义之前,可否先搞清楚其行为。到底private继承的行为如何呢?
如果class之间的继承关系是private,编译器不会自动将一个derived class对象(例如Student)转换成一个base class对象(例如Person)。
这和public继承的情况不同。
第二条规则是,由private base class继承而来的所有成员,在derived class中都会变成private属性,
纵使它们在base class中原本是protected或public属性。
现在让我们开始讨论其意义,
private继承意味着implement-in-terms-of(根据某物实现出)。
如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,
不是因为B对象和D对象存在任何观念上的关系。
private继承纯粹只是一种实现技术,
这就是为什么继承自一个private base class的每样东西在你的class内部都是private,因为它们都只是实现枝节而已。
private继承意味只有实现部分被继承,接口部分应略去。
如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。
private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。
private继承意味is-implement-in-terms-of(根据某物实现出),
这个事实有点令人不安,因为复合(compositoin)的意义也是这样。
你如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。
何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。
3. 例子
假设我们的程序涉及Widget,而我们决定应该较好的了解如何使用Widget,
例如我们不只想要知道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接口。
我们必须以private形式继承Timer:
class Widget: private Timer{
private:
// 查看Widget的数据,等等
virtual void onTick() const;
};
籍由private继承,Timer的public onTick函数在Widget内变成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)。
坦白说,我展示它主要是为了提醒你,解决一个设计问题的方法不只一种,而训练自己思考多种做法是值得的。
4. 空白基类优化
有一种激进情况涉及空间最优化,可能会促使你选择“private继承”而不是“继承加复合”。
这个激进情况真是有够激进,只适用于你所处理的class不带任何数据时。
这样的class没有non-static成员变量,没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr),
也没有virtual base class(因为这样的base class也会招致体积上的额外开销)。
于是这种所谓的empty class对象不使用任何空间,因为没有任何隶属对象的数据需要存储。
然而,由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小,所以如果你这样做,
// 没有数据,所以其对象应该不使用任何内存
class Empty{};
// 应该只需要一个int空间
class HoldsAnInt{
private:
int x;
// 应该不需要任何内存
Empty e;
};
你会发现sizeof(HoldsAnInt) > sizeof(int)
,一个Empty成员变量竟然要求内存。
在大多数编译器中sizeof(Empty)
获得1,因为面对“大小为零之独立(非附属)对象”,通常C++官方勒令默默安插一个char到空对象内。
然而,齐位需求(alignment)可能造成编译器为类似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 class身上。
现实中的“empty” class并不是真的是empty,
虽然它们从未拥有non-static成员变量,却往往内含typedef,enum,static成员变量,或non-virtual函数。
STL就有许多技术用途的empty class,其中内含有用的成员(通常是typedef),包括base class unary_function
和binary_function
,
这些是“用户自定义之函数对象”通常会继承的class,感谢EBO的广泛实践,使这样的继承很少增加derived class的大小。
尽管如此,让我们回到根本,
大多数class并非empty,所以EBO很少成为private继承的正当理由。
更进一步说,大多数继承相当于is-a,这是指public继承,不是private继承。
复合和private继承都意味着is-implemented-in-terms-of,但复合比较容易理解,
所以无论什么时候,只要可以,你还是应该选择复合。