zoukankan      html  css  js  c++  java
  • Effective C++ —— 继承与面向对象设计(六)

    条款32 : 确定你的public继承塑模出is-a关系

      以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系。请务必牢记。当你令class D 以public形式继承class B,你便是告诉C++编译器,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。is-a关系只对public继承才成立,private继承的意义于此完全不同(条款39),至于protected继承,至今仍困惑着我。考虑如下例子:

    class Person { ...  };
    class Student : public Person { ... };

      每个学生都是人,但并非每一个都是学生。对人可以成立的每一件事,对学生也都成立。任何函数如果期望获得一个类型为Person(或pointer-to-Person 或 reference-to-Person)的实参,也都愿意接受一个Student对象(或pointer-to-Student或 reference-to-Student)。

      然而,考虑这种情况:企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如此,下面代码:

    class Bird
    {
        public:
            virtual void fly();    // 鸟可以飞
            .....
    };
    
    class Penguin:public Bird  // 企鹅是一种鸟
    {
        .....
    };

      有问题:这个继承体系说企鹅可以飞!!我们对上述代码作出如下修改(Method1):

    class Bird
    {
        .....            // 没有声明fly函数
    };
    class FlyingBird:public Bird
    {    
        public:
            virtual void fly();
            .....
    };
    class Penguin:public Bird  // 企鹅是一种鸟
    {
        .....    // 没有声明fly函数
    };

      此刻,这样的继承体系较好的满足了我们的真正意思。然而,对某些系统而言,可能不需要区分会飞的鸟和不会飞的鸟,原先修改之前的“双classes继承体系”就已经满足需求了。这反映出一个事实:世界上并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事,包括现在与未来

      对原先代码的令一种修改如下(Method2):

    void error(const std::string& msg);     // 定义于另外某处
    class Penguin:public Bird  // 企鹅是一种鸟
    {
        virtual void fly() { error("Attempt to make a penguin fly!"); }      // 运行期发生错误
        .....  
    };

      注意:这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”。Method2是在程序运行期发生错误,然而,我们知道,把错误提前到编译器是较佳的选择

    class Bird
    {
        ........ // 没有声明fly函数
    };
    class Penguin:public Bird
    {
        ........ // 没有声明fly函数
    };
    // 那么下面调用会使编译器报错
    Penguin p;
    p.fly();    // 错误

      is-a并非是唯一的classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C++中并不罕见,所以,你务必了解这些“classes相互关系”之间的差异。

    故而:

      “public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。

    条款33 : 避免遮掩继承而来的名称

      derived classes内的名称会遮掩base classes内的名称,即使base classes 和 derived classes 内的函数有不同的参数类型也适用,而且不论函数式virtual 或 non-virtual一体适用。在public继承下从来没有人希望如此。考虑如下代码:

    class Base
    {
        private:
            int x;
        public:
            virtual void mf1() = 0;
            virtual void mf1(int);
            virtual void mf2();
            void mf3();
            void mf3(double);
            .....
    };
    class Derived : public Base
    {
        public:
            virtual void mf1();
            void mf3();
            void mf4();
            .....
    };
        
    //考虑下面调用
    Derived d;
    int x;
    ....
    d.mf1();        // ok,调用Derived::mf1
    d.mf1(x);      // error, 因为Derived::mf1遮掩了Base::mf1
    d.mf2();        // ok, 调用Base::mf2
    d.mf3();        // ok, 调用Derived::mf3
    d.mf3(x);        //error, 因为Derived::mf3遮掩了Base::mf3

      Method1:如果你继承base class 并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩(using声明式会令继承而来的某给定名称之所有同名函数在derived class 中都可见。)。如下修改上述代码:

    class Base
    {
        private:
            int x;
        public:
            virtual void mf1() = 0;
            virtual void mf1(int);
            virtual void mf2();
            void mf3();
            void mf3(double);
            .....
    };
    class Derived : public Base
    {
        public:
            using Base::mf1;        //    让Base class内名为mf1 和mf3的所有东西
            using Base::mf3;        // 在Derived 作用域内都可见        
            virtual void mf1();
            void mf3();
            void mf4();
            .....
    };
        
    //考虑下面调用
    Derived d;
    int x;
    ....
    d.mf1();        // ok,调用Derived::mf1
    d.mf1(x);      // ok, 调用Base::mf1
    d.mf2();        // ok, 调用Base::mf2
    d.mf3();        // ok, 调用Derived::mf3
    d.mf3(x);        //ok, 调用Base::mf3

      Method2:有时你并不想继承base classes的所有函数(当然,这在public继承下是绝对不允许的,因为它违反了public继承的“is-a”关系),这时可以采用一个简单的转交函数

    class Base
    {
            .....     // 与前同
    };
    class Derived : public Base
    {
        public:
            virtual void mf1()          // 转交函数
            { Base::mf1(); }      // 暗自成为inline,条款30
    };
        
    //考虑下面调用
    Derived d;
    int x;
    ....
    d.mf1();        // ok,调用Derived::mf1
    d.mf1(x);      // error, Base::mf1() 被遮掩了

    故而:

      1. derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。

      2. 为了让被遮掩的名称再见天日,可使用using声明式或转交函数。

    条款34: 区分接口继承和实现继承

      1. 成员函数的接口总是会被继承。条款32所说,public继承意味is-a(是一种)。

      2. 声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口。令人意外的是,C++竟然允许我们为pure virtual函数提供定义,但调用它的唯一途径是“调用时明确指出其class名称”。(baseObj->BaseName::virtualFuncName();)

      3. 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑如下代码:

    // 现有A型、B型两种飞机,以相同方式飞行
    class Airport
    class Airplane
    {
        public:
            virtual void fly(const Airport& destination);
        ....
    };
    void Airplane::fly(const Airport& destination)
    {
        // 缺省代码
    }
    
    class ModelA:public Airplane { ... };
    class ModelB:public Airplane { ... };
    
    //现有一C型飞机,以不同于A、B型的飞行方式飞行
    class ModelC:public Airplane { ... };   //竟然忘记重新定义C型飞机的fly函数
    
    // 如下调用会导致灾难
    Airport pdx(...);
    Airplane* pa = new ModelC;
    ...
    pa->fly(pdx);     // 

      上面代码的问题不在Airplane::fly有缺省行为,而在于ModelC在未明白说出“我要”的情况下就继承了该缺省行为(而此缺省行为却不是ModelC想要的)。我们必须做到“提供缺省实现给derived classes,但除非它们明白要求否则免谈”,解决方法在于切断“virtual函数接口”和其“缺省实现”之间的连接。可作如下修改:

    // 现有A型、B型两种飞机,以相同方式飞行
    class Airport
    class Airplane
    {
        public:
            virtual void fly(const Airport& destination) = 0; // 纯虚函数,迫使子类定义自己的实现
        ....
        protected:
            void defaultFly(const Airport& destination);
    };
    void Airplane::defaultFly(const Ariport& destination)
    {
        // 缺省实现
    }
    
    class ModelA:public Airplane 
    { 
        public:
            virtual void fly(const Airport& destination)     // 
            { defaultFly(destination); }
            ... 
    };
    class ModelB:public Airplane 
    { 
        public:
            virtual void fly(const Airport& destination)
            { defaultFly(destination); }
            ... 
    };
    
    class ModelB:public Airplane 
    { 
        public:
            virtual void fly(const Airport& destination);
            ... 
    };
    void ModelC::fly(const Airport& destination)
    {
        // 实现C型飞机特有的飞行方式
    }

      3. 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现

    故而:

      1. 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。

      2. pure virtual函数只具体指定接口继承。

      3. 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。

      4. non-virtual 函数具体指定接口继承以及强制性实现继承。

    条款35: 考虑virtual函数以外的其他选择

      假设我们有如下代码,其中有一个virtual函数:

    class GameCharacter
    {
        public:
            virtual int healthValue() const;  // 返回游戏角色血量
            ...
    };

      Method1. 藉由NVI(Non-Virtual Interface)手法实现Template Method 模式(一种设计模式)

    class GameCharacter
    {
        public:
            int healthValue() const
            {
                ...             // 做一些事前工作
                int retVal = doHealthValue();
                ...             // 做一些事后工作
                return retVal;
            }
            ...
        private: // 私有,禁止外部访问(禁止类对象调用,只能在类内部被使用)
         // 私有虚函数,派生类可以重新定义它,healthValue函数为公有非虚函数,派生类会继承此函数.
        但是,父类和派生类对象都 能且只能 通过调用healthValue函数来间接调用私有虚函数,从而保证了基类对于私有虚函数的绝对控制(可以保证在私有虚函数之前/之后做一些工作,
         并且不会因为派生类重新定义私有虚函数而丧失这种保证,因为派生类对象同样只能通过基类的healthValue函数调用到自己的私有虚函数)

         //
    NVI手法允许derived classes重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利. virtual int doHealthValue() const // derived classes可重新定义它 { ... } ... };

      这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。我把这个non-virtual函数称为virtual函数的外覆器。

      NVI手法的一个优点隐式在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清除场景。

      注意,NVI手法涉及在derived classes内重新定义private virtual函数。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI手法允许derived classes重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权利

      Method2(函数指针). 藉由Function Pointers实现Strategy模式:角色构造函数接受一个指针,指向一个健康计算函数(函数指针)。

    class GameCharacter;   // 前置声明
    // 以下函数是计算健康指数的缺省算法
    int defaultHealthCalc(const GameCharacter& gc);
    class GameCharacter
    {
        public:
            typedef int (*HealthCalcFunc)(const GameCharacter&); // 函数指针HealthCalcFunc
            explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
                : healthFunc(hcf)
            {}
    
            int healthValue() const
            { return healthFunc(*this); }
            ...
        private:
            HealthCalcFunc healthFunc;
    };

      这个做法是常见的Strategy设计模式的简单应用,它具有以下特点:

      (1)优点:同一个游戏角色之不同实体可以有不同的健康计算函数。

    class EvilBagGuy : public GameCharacter
    {
        public:
            explicit EvilBagGuy(HealthCalcFunc hcf = defaultHealthCalc)
                : GameCharacter(hcf)
            { ... }
            ... 
    };
    int loseHealthQuickly(const GameCharacter&);
    int loseHealthSlowly(const GameCharacter&);
    // 不同角色搭配不同计算方式
    EvilBagGuy ebg1(loseHealthQuickly);   
    EvilBagGuy ebg2(loseHealthSlowly);

      (2)优点:游戏角色健康计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalcator,用来替换当前的健康计算函数。

      (3)缺点:角色的健康计算需要用到class 内的non-public信息的时候就会有问题(non-member 函数无权访问 non-public 成分)。唯一能够解决“需要以non-member函数访问class 的non-public成分”的办法就是:弱化class 封装。例如class可声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数(getter函数)

      Method3(函数对象). 藉由tr1::function 完成Strategy模式:tr1::function对象可以包装任何可调用物(也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。具体可参见C++11新特性之八——函数对象function。可对Method2代码修改成使用tr1::function:

    class GameCharacter;   // 前置声明
    int defaultHealthCalc(const GameCharacter& gc);
    class GameCharacter
    {
        public:
            // HealthCalcFunc可以是任何“可调用物”,可被调用并接受
            // 任何兼容于GameCharacter之物,返回任何兼容于int的东西。详下:
            typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
            explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
                : healthFunc(hcf)
            {}
    
            int healthValue() const
            { return healthFunc(*this); }
            ...
        private:
            HealthCalcFunc healthFunc;
    };

      和前一个设计(其GameCharacter持有的是函数指针)比较,这个设计几乎相同。唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于一个指向函数的泛化指针。这时,角色构造函数能接受的事物更具弹性:

    short calcHealth(const GameCharacter&);      // 普通计算函数,返回non-int
    
    struct HealthCalculator       // 使用clas类似 
    {
        int operator() (const GameCharacter&) const      // 重写()操作符
        { ... }
    };
    
    class GameLevel
    {
        public:
            float health(const GameCharacter&) const;      //成员函数,返回non-int
            ...
    };
    
    class EvilBagGuy : public GameCharacter
    {
        ....
    };
    
    class EyeCandyCharacter : public GameCharacter
    {
        ....
    };
    
    EvilBagGuy ebg1(calcHealth);    // 普通函数
    EyeCandyCharacter ecc1(HealthCalculator());    //函数对象
    GameLevel currentLevel;
    ... 
    EvilBagGuy ebg2(                // 成员函数
        std::tr1::bind(&GameLevel::health,
                                currentLevel,
                                _1)
    );

      这里需要对上述代码中的tr1::bind作下解析:首先表明,为计算ebg2的健康指数,应该使用GameLevel class 成员函数health。GameLevel::health 宣称它自己接受一个参数(那是个reference指向GameCharacter),但它实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter(这个对象将被计算出健康指数)。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter 和一个 GameLevel),转而接受单一参数(一个GameCharacter)。在这个例子中我们必然会想要使用currentLevel作为“ebg2的健康计算函数所需的那个GameLevel对象”,于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为:它指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。tr1::bind具体见C++11新特性之二——std::bind  std::function 高级用法

      Method4(接口类)古典的Strategy模式:本例中为,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。可画出UML设计图

    class GameCharacter;   // 前置声明
    class HealthCalcFunc
    {
        public:
            ...
            virtual int calc(const GameCharacter& gc) const
            { ... }
            ...
    };
    HealthCalcFunc defaultHealthCalc;
    
    class GameCharacter
    {
        public:
            explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
                : pHealthFunc(phcf)
            {}
    
            int healthValue() const
            { return pHealthFunc->calc(*this); }
            ...
        private:
            HealthCalcFunc* pHealthFunc;
    }

      这个做法典型的采用了Strategy模式。另外,它还提供“将一个既有的健康算法纳入使用”的可能性——只要为HealthCalcFunc继承体系添加一个derived class即可。

    小结:
      1. 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性的virtual函数。

      2. 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。

      3. 以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。

      4. 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。

    故而:

      1. virtual函数的替代方案包括NVI手法即Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

      2. 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class 的non-public成员。

      3. tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。

    条款36: 绝不重新定义继承而来的non-virtual函数

      考虑下面代码:

    class B
    {
        public:
            void mf();
            ....
    };
    class D : public B
    {
        public:
            void mf();     // 遮掩B::mf,条款33
            .....
    };
    
    D x;    // D 对象
    B* pB = &x;
    D* pD = &x;
    // 针对D对象x,下面语句竟然调用的是不同的mf
    pB->mf();      // 调用B::mf
    pD->mf();      // 调用D::mf

      造成此一两面行为的原因是,non-virtual函数如B::mf 和 D::mf都是静态绑定(条款37)。这意思是,由于pB被声明为一个pointer-to-B,通过 pB调用的 non-virtual 函数永远是B所定义的版本,即使pB指向一个类型为”B派生之class”的对象。更明确地说,当mf(base class 内为 non-virtual 并在derived class 内被重载的函数)被调用,如何一个D对象都可能表现出B或D的行为;决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。References的行径类似。(virtual函数是动态绑定,所以不受这个问题之苦,条款07也解释过为什么动态性base classes内的析构函数应该是virtual.)。

    参见C++易混淆知识点整理 第3点

    故而:

      绝对不要重新定义继承而来的non-virtual函数。

    条款37: 绝不重新定义继承而来的缺省参数值

       让我们一开始就将讨论简化。你只能继承两种函数:virtual和non-virtual函数。然而重新定义一个继承而来的non-virtual函数永远是错误的(条款36),所以我们可以安全地将本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”。这种情况下,本条款成立的理由就非常直接而明确了:virtual函数系动态绑定,而缺省参数值却是静态绑定

    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*, 动态类型为Circle*
    Shape* pr = new Rectangle;            // 静态类型为Shape*, 动态类型为Rectangle*
    
    // 动态类型一如其名称所示,可在程序执行过程中改变(通常经由赋值完成)
    ps = pc;        // ps 的动态类型如今是Circle*
    ps = pr;            // ps 的动态类型如今是Rectangle*

      对象的所谓“静态类型”是指它在程序中被声明时所采用的类型(初始)。对象的所谓“动态类型”是指“目前所指对象的类型”(当前)。针对上面代码,考虑如下调用:

    // 下面调用正常
    pc->draw(Shape::Red);        // 调用Cricle::draw(Shape::Red)
    pr->draw(Shape::Red);        // 调用Rectangle::draw(Shape::Red)
    // 然而,下面调用则会出现问题
    // 一如上面所说,virtual函数是动态绑定,而缺省参数值却是静态绑定
    // 下面调用,你可能会在“调用一个定义于derived class内的virtual函数”的同时
    // 却使用base class为它所指定的缺省参数值:
    pr->draw();        // 调用Rectangle::draw(Shape::Red)

      此例子中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数(动态绑定),一如你所预期。Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!(即使指针换成reference,问题依然存在)。

      C++为什么坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢而且更复杂。

      如果,客户想尝试遵守这条规则,并且同时提供缺省参数值给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;
            ...
    };

      不幸的是,上面这样的代码导致代码重复。更糟的是,代码重复又带着相依性:如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变,否则它们最终会导致“重复定义一个继承而来的缺省参数值”。条款35列了不少virtual函数的替代设计,其中之一是NVI(non-virtual interface)手法:令base classes 内的一个public non-virtual 函数调用private virtual函数,后者可被derived classes重新定义。可作如下修改:

    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;  //注意,不须指定缺省参数值,但必须提供实现(可实现子类特定的颜色),因为base class 内为纯虚函数(pure virtual)
            ... 
    };

    由于non-virtual 函数应该绝对不被derived classes覆写(条款36),这个设计很清楚地使得draw函数的color缺省参数值总是Red。
    故而:

      绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你唯一应该覆写的东西——却是动态绑定。

    条款38: 通过复合塑模出has-a或“根据某物实现出”

       复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味has-a(有一个,包含关系)或 is-implemented-in-terms-of(根据某物(底层)实现出(适配器),类似STL的容器适配器如Stack,Queue等)。那是因为你正打算在你的软件中处理两个不同领域(domains)。程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,如缓冲区、互斥器等等。这些对象相当于你的软件的实现域。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

      区分is-a和has-a并不困难,比较麻烦的是区分is-a(是一种)和 is-implemented-in-terms-of(根据某物实现出)这两种对象关系。  

      假如我们现在需要一组classes用来表现由不重复对象组成的sets。那么,考虑如下操作:
      1. 复用标准程序库提供的set template。不幸的是,标准程序库的set以平衡查找树实现,使它们在查找、安插、移除元素保证拥有对数时间效率,但空间销毁较大。而假设我们的程序,空间比效率更重要,那么标准程序库set template便不满足;

      2. 让你的set template继承std::list,毕竟实现sets的一种方法,便是在底层采用linked lists(链表)。而刚好,标准程序库有一个list template。然而, public继承中,一如条款32所说,对基类为真的每一件事,对派生类也应该为真。但list 可以内含重复元素,而我们的set,很不幸,不能内含重复元素。由于两个classes之间并非is-a关系,所有public继承不适合用来塑模它们。

      解决方案Set对象可根据一个list对象实现出来。is-implemented-in-terms-of(根据某物实现出,是指使用了某物的特性,但又不完全相同,因为我们可能额外添加了一些特性,也可能在利用某物特性之前和之后做一些特殊操作,外覆在“某物特性”上面。)

    template<class T>
    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 及标准程序库其他部分提供的机能来完成
    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 见条款42
        typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);
        if (it != rep.end()) rep.erase(it);
    }
    
    template<typename T>
    std::size_t Set<T>::size(const T& item) const
    {
        return rep.size();
    }

    故而:
      1. 复合的意义和public继承完全不同。

      2. 在应用域,复合意味has-a(有一个)。在实现域,复合意味is-implemented-in-terms-of(根据某物实现出)。

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

      private继承意味is-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 继承在软件“设计”层面上没有意义,其意义只及于软件“实现”层面。
      条款38 指出复合的意义也是is-implemented-in-terms-of(根据某物实现出)。对于这两者,尽可能使用复合,必要时才使用private继承。何时才必要呢?主要是当protected 成员(派生类能访问到父类的protected 成员,所以必须是在类的继承体系之下;而复合不属于类的继承体系下)和/或 virtual 函数(virtual 函数会导致动态绑定,这也需要在类的继承体系之下完成)
       考虑这样一种情况:假设我们有个Widget class, 现在我们想在运行期间周期性的记录审查它的每个函数被调用次数,此刻,我们需要某种定时器,代码如下:

    class Timer
    {
        public:
            explicit Timer(int tickFrequency);
            virtual void onTick() const;   // 定时器每滴答一次,此函数就被自动调用一次
            ....
    };
    
    // Method1:以private形式继承Timer
    class Widget : private Timer
    {
        private:
            virtual void onTick() const;       
            ...
    };
    
    // Method2:public 继承 加 复合
    class Widget
    {
        private:
            class WidgetTimer : public Timer
            {
                public:
                    virtual void onTick() const;
                    ...
            };
            WidgetTimer timer;
            ....
    };

      这里,我们可能会选择Method2,原因:

      (1)你或许回想设计Widget 使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget 继承自Timer,上面的想法就不可能实现,即使是private 继承也不可能(条款35 曾说过,derived classes 可以重新定义 virtual函数,即使它们不得调用(virtual 是私有)),但如果WidgetTimer 是 Widget 内部的一个private 成员并继承 Timer,Widget 的derived classes 将无法取用WidgetTimer(派生类 也无法访问 基类的 private 成员),因此无法继承它或重新定义它的virtual函数。

      (2)你或许会想将Widget 的编译依存性降至最低。如果Widget 继承Timer,当Widget 被编译时Timer 的定义必须可见,所以定义Widget 的那个文件恐怕必须#include Timer.h。但如果WidgetTimer 移出Widget之外而Widget内含指针指向一个WidgetTimer,Widget 可以只带着一个简单的WidgetTimer 声明式,不再需要#include 任何与Timer有关的东西,条款31。

       有一种特殊情况,可能会促使你选择“private 继承” 而不是 “继承加复合”:你所处理的class不带任何数据。这样的class (1)没有non-static成员变量,(2)没有virtual函数(因为这种函数的存在会为每个对象带来一个vptr,条款07),(3)也没有virtual base classes(因为这样的base classes 也会招致体积上的额外开销,条款40)。于是这种所谓的empty classes 对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而由于技术上的理由,C++裁定凡是独立(非附属)对象都必须有非零大小,所以,如果你这样做:

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

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

      但或许你注意到了,我很小心地说“独立(非附属)”对象的大小一定不为零。也就是说,这个约束不适用于derived class对象内的base class成分,因为它们并非独立(非附属)。如果你继承Empty,而不是内含一个那种类型的对象:

    class Empty { };     // 没有数据,所以其对象应该不使用任何内存
    
    class HoldsAnInt : private Empty
    {
        private:
            int x;
    };

      几乎可以确定sizeof(HoldsAnInt) > sizeof(int)。这是所谓的EBO(empty base optimization,空白基类最优化,我试过所有的编译器都有这样的结果。值得注意的是,EBO 一般只在单一继承(而非多重继承)下才可行

      现实中的“empty”classes并不真的是empty。虽然它们从未拥有non-static成员变量,却往往内含typedefs,enums,static成员变量,或non-virtual函数STL 就有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括base classes unary_function 和 binary_function ,这些是“用户自定义之函数对象”通常会继承的classes。EBO的广泛实践,使这样的继承很少增加derived classes的大小。

    故而:

      1. Private 继承意味 is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当derived class需要访问 protected base class 的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。即便如此,一个混合了public继承和复合的设计,也能达成你所要的行为。

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

    参见 C++易混淆知识点整理第10点

    条款40: 明智而审慎地使用多重继承

       (1) 首先,需要认清一个事实,当多重继承(multiple inheritance;MI)进入设计景框,程序有可能从一个以上的base classes 继承相同的名称(如函数,typedef等等),那会导致较多的歧义机会

    class BorrowableItem
    {
        public:
            void checkOut();      //
            ....
    };
    class ElectronicGadget
    {
        private:
            bool checkOut() const;
            .....
    };
    class MP3Player : 
        public BorrowableItem,
        public ElectronicGadget
    { .... };
    
    MP3Player mp;
    mp.checkOut();        // 歧义,调用的是哪个checkOut ?

      注意此例中对checkOut的调用是歧义(模棱两可)的,即使两个函数之中只有一个可取用(BorrowableItem内的checkOut是public, ElectronicGadget内的却是private)。这与C++用来解析重载函数调用的规则相符:在看到是否有个函数可取用之前,C++ 首先确认这个函数对此调用而言是最佳匹配。找出最佳匹配函数后才检查其可取用性(是否最佳匹配—>是否可取用)。本例的两个checkOuts有相同的匹配程度(因此才造成歧义),没有所谓最佳匹配。因此ElectronicGadget::checkOut的可取用性也就从未被编译器审查。

      为了解决这个歧义,你必须明白指出你要调用哪一个base class 内的函数:

    mp.BorrowableItem::checkOut();    // 明白调用此函数
    // 尝试调用ElectronicGadget::checkOut 将会获得一个“尝试调用private成员函数”的错误

      (2) 多重继承可能导致“钻石型多重继承”(菱形继承):

    class File { ... };
    class InputFile : public File { ... };
    class OutputFile : public File { ... };
    class IOFile : public InputFile, public OutputFile
    { ... };

      这样,File 中的成员变量将沿着InputFile和OutputFile两条路径被复制,最终在IOFile中造成两份成员变量。而C++缺省也是这么做的。如果你只需要在IOFile中保留一份File的成员变量,那么你必须令File 成为一个 virtual base class(虚基类,不同于抽象基类)。为了这样做,你必须令所有直接继承自它的classes 采用“virtual继承”(虚继承导致虚基类,保留基类的成员变量至多一份。):

    class File { ... };
    class InputFile : virtual public File { ... };
    class OutputFile : virtual public File { ... };
    class IOFile : public InputFile, public OutputFile
    { ... };

      virtual 继承所需要付出的成本是巨大的:首先,使用virtual 继承的那些classes 所产生的对象往往比使用non-virtual 继承的兄弟们的体积大,访问virtual base classes 的成员变量时,也比访问non-virtual base classes的成员变量速度慢。其次,virtual base 的初始化责任是由继承体系中的最低层 class 负责,这暗示:(1)classes 若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

      所以,对于virtual base class(亦相当于使用non-virtual继承):第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base class,尽可能避免在其中放置数据。Java 和 .NET 的Interfaces 便是不允许含有任何数据

      (3)下面代码说明多重继承也有其合理用途,塑模“人”的C++ Interface class(条款31):

    // 多重继承合理用途例子:
    // 这个类指出需要实现的接口
    class IPerson
    {
        public:
            virtual ~IPerson();
            virtual std::string name() const = 0;
            virtual std::string birthDate() const = 0;
    };
    
    class DataBaseID  { ... };
    
    // 用以实现IPerson接口的类,将被private继承
    class PersonInfo 
    {
        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;
            ....
    };
    
    // CPerson 必须实现IPerson接口,需得以public 继承才能完成
    // CPerson 需要重新定义valueDelimOpen 和valueDelimClose,
    // 单纯复合无法应付,这里我们采用private 继承。
    // private 继承自 PersonInfo,导致PersonInfo中的virtual函数在CPerson成为Private成员,和在CPerson中定义Private实现细目类似 class CPerson : public IPerson, private PersonInfo // 多重继承 { public: explicit CPerson(DatabaseID pid) : PersonInfo(pid) { } virtual std::string name() const // 实现必要的IPerson成员函数 { return PersonInfo::theName(); } // theName 内调用valueDelimOpen(Close)函数 virtual std::string birthDate() const { return PersonInfo::theBirthDate(); } private: const char* valueDelimOpen() const { return ""; } // 重新定义继承而来的virtual函数 const char* valueDelimClose() const { return ""; }
      private:
         // std::string nameStr; // 若有实现细目,置于具象类中并为private };

      有了上面的interface class ,客户便可以使用factory function(工厂函数,条款31)将“派生自IPerson 的具象classes”实体化

    // factory function,根据一个独一无二的数据库ID 创建一个Person对象,条款18 告诉你为什么返回智能指针
    // 也可以将createPerson声明在接口类IPerson中并使其成为static(接口类无法创建对象)。
    std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier);
    // 如下createPerson实现
    std::tr1::shared_ptr<IPerson> createPerson(DatabaseID personIdentifier)
    {
        return
         // 由于CPerson public继承自 IPerson(为is-a关系) std::tr1::shared_ptr<IPerson> (new CPerson(personIdentifier)); } // 如下调用createPerson DatabaseID askUserForDatabaseID(); DatabaseID id(askUserForDatabaseID()); ... std::tr1::shared_ptr<IPerson> pp(createPerson(id));

      注意:条款31中所描述的情况是,为了把class中的实现细目抽离出来,我们可以实现出handle class 或 interface class,以达到降低编译相依度的目的。本条款(3)中在此基础上还增加了对virtual函数的处理,为了同时将virtual函数抽离,我们private继承 PersonInfo。

    故而:
      1. 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承(虚基类)的需要。

      2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class 不带任何数据,将是最具实用价值的情况。

      3. 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private 继承某个协助实现的class”的两相组合。

  • 相关阅读:
    Windows Store App 主题动画
    Windows Store App 过渡动画
    Windows Store App 控件动画
    Windows Store App 近期访问列表
    Windows Store App 文件选取器
    Windows Store App 访问应用内部文件
    Windows Store App 用户库文件分组
    Windows Store App 获取文件及文件夹列表
    Windows Store App 用户库文件夹操作
    Windows Store App 用户库文件操作
  • 原文地址:https://www.cnblogs.com/yyxt/p/4812135.html
Copyright © 2011-2022 走看看