美文网首页C++
[C++] public和private继承

[C++] public和private继承

作者: 何幻 | 来源:发表于2017-05-04 17:49 被阅读147次

    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

    相关文章

      网友评论

        本文标题:[C++] public和private继承

        本文链接:https://www.haomeiwen.com/subject/ypddtxtx.html