[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_functionbinary_function
这些是“用户自定义之函数对象”通常会继承的class,感谢EBO的广泛实践,使这样的继承很少增加derived class的大小。

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


Effective C++ - P187

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容