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

若class之间的继承关系是private,则编译器不会自动将一个derived class对象转换为一个base class对象。

由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,因为它们都只是实现枝节而已)。

private继承意味只有实现部分被继承,接口部分应略去。

若D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他含义了。private继承在软件“设计”层面上没有意义,其意义只及于软件实现层面。

private意味implemented-in-terms-of,但上篇文章指出复合的意义也是这样。

该如何取舍?

主要是当protected成员和/或virtual函数牵扯进来的时候。还有一种情况,当空间方面的利害关系足以踢翻private继承的支柱时。

假设一个程序设计Widget,而我们决定应该较好地了解如何使用Widget。

例如我们不只想知道Widget成员函数多么频繁被调用,也想知道经过一段时间后调用比例如何变化。

我们决定修改Widget class,让它记录每个成员函数的被调用次数。运行期间我们将周期性地审查那份信息,也许再加上每个Widget的值,以及我们需要评估的任何其他数据。

为完成这项工作,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了:

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

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

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

但private并非绝对必要。若我们决定以复合取而代之,只要在Widget内声明一个嵌套式priavte class,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型对象于Widget内。

下面是这种解法的草样:

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

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

在这里展示它主要是为了提醒你,解决一个设计问题的方法不只一种,而训练自己思考多种做法是值得的。

可以想出两个理由,为什么你可能愿意(或说应该)选择这样的public继承加复合,而不是选择原先的private继承设计。

第一,你或许会想设计Widget使它得以拥有derived class,但同时你可能会想阻止derived class重新定义onTick。

若Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但若WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived class将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。

第二,你或许会想要将Widget的编译依存性降至最低。

若Widget继承Timer,当Widget被编译时Timer的定义必须可见,所以定义Widget的那个文件恐怕必须#include Timer.h。但若WidgetTimer移出Widget之外而Widget内含指针指向一个WidgetTimer,Widget可以只带着一个简单的WidgetTimer声明式,不再需要#include任何与Timer有关的东西。

对大型系统而言,如此的解耦可能是重要的措施。

上文提过,有一种激进情况设计空间最优化,可能会促使你选择“private继承”而非“继承加复合”。

这个激进情况只适用于你所处理的class不带任何数据时。

这样的class没有non-static成员变量,没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr),也没有virtual base class(因为这样的base class也会招致体积上的额外开销)。于是这种所谓的empty class对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小。

所以若你这样做:

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

你会发现sizeof(HoldsAnInt)>sizeof(int)。一个Empty成员变量竟然要求内存。

在大多数编译器中sizeof(Empty)获得1,因为面对“大小为零的独立(非附属)对象”,通常C++官方勒令默默安插一个char到空对象内。然而,齐位需求可能造成编译器为类似HoldsAnInt这样的class加上一些衬垫,所以有可能HoldsAnInt对象不只获得一个char大小,也许实际上被放大到足够又存放一个int。

但这个约束不适用于derived class对象内的base class成分,因为它们并非独立。若你继承Empty,而不是内含一个那种类型的对象:

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

几乎可以确定sizeof(HoldsAnInt)==sizeof(int)。这是所谓的EBO(empty base optimization;空白基类最优化)。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。

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

当你面对“并不存在is-a关系”的两个class,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。

即便如此,你也已经看到,一个混合了public继承和复合的设计,往往能够释出你要的行为,尽管这样的设计有较大的复杂度。

“明智而审慎地使用private继承”意味,在考虑过所有其他方案之后,若仍然认为private继承是“表现程序内两个class之间的关系”的最佳方法,这才用它。

总结

1.private继承意味is-implemented-in-terms-of。它通常比复合的级别低。但当derived class需要访问protected class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

2.和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序开发者而言,可能很重要。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值