• Effective C++: 06继承与面向对象设计


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

    以C++进行面向对象编程,最重要的一个规则是:public继承表示的是"is-a"(是一种)的关系。

    如果令class D以public形式继承class B,你便是告诉编译器说,每一个类型为D的对象同时也是一个类型为B的对象,但是反之不成立。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。

    具体到代码上,任何函数如果期望获得一个类型为B(或pointer-to-B或reference-to-B)的实参,都也愿意接受一个D对象(或pointer-to-D或reference-to-D)。

    这个论点只对pubiic继承才成立。private继承的意义与此完全不同(见条款39),至于protected继承,那是一种其意义至今仍然困惑我的东西。

     

    public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子:class square应该以public形式继承class Rectangle吗?每个人都知道正方形是一种矩形,反之则不一定,这是真理,但是看下面的代码:

    class Rectangle {
    public:
      virtual void setHeight(int newHeight);
      virtual void setWidth(int newWidth);
    
      virtual int height() const;               // return current values
      virtual int width() const;
    };
    
    void makeBigger(Rectangle& r)               // function to increase r's area
    {
      int oldHeight = r.height();
      r.setWidth(r.width() + 10);               // add 10 to r's width
      assert(r.height() == oldHeight);          // assert that r's
    }  

    显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度,r的高度从未被更改。

    现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:

    class Square: public Rectangle {...};
    
    Square s;
    assert(s.width() == s.height());
    makeBigger(s);
    assert(s.width() == s.height());

    很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的宽度和其高度相同。但现在我们遇上了一个问题。我们如何调解下面各个assert判断式:调用makeBigger之前,在makeBigger函数内s的高度和宽度相同;s的宽度改变,但高度不变;makeBigger返回之后,s的高度再度和其宽度相同。

    本例的根本困难是,某些可施行于矩形身上的事情却不可施行于正方形身上。但是public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。在正方形和矩形例子中,那样的主张无法保持,所以以public继承塑模它们之间的关系并不正确。

     

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

             1:下面的代码是一个很简单的名称遮掩的例子:

    int x;
    
    void someFunc()
    {
      double x;
      std::cin >> x;
    }

    someFunc的x是double类型而global x是int类型,但那不要紧。C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否对应相同的类型,并不重要。本例中一个名为x的double遮掩了一个名为x的int。

     

    2:导入继承之后,当派生类成员函数内引用(refer to)基类内的某物(成员函数、typedef、或成员变量)时,编译器可以找出我们所refer to的东西,因为派生类继承了声明于基类内的所有东西。实际运作方式是,派生类作用域被嵌套在基类作用域内,像这样:

    class Base {
    private:
      int x;
    
    public:
      virtual void mf1() = 0;
      virtual void mf2();
      void mf3();
    };
    
    class Derived: public Base {
    public:
      virtual void mf1();
      void mf4();
    };
    
    void Derived::mf4()
    {
      mf2();
    }

    此例内含一组混合了public和private名称,以及一组成员变量和成员函数名称。这些成员函数包括pure virtual,impure virtual和non-virtual三种,这是为了强调我们谈的是名称,和其他无关。这个例子也可以加入各种名称类型,例如~,nested classes和typedef。整个讨论中唯一重要的是这些东西的名称,至于这些东西是什么并不重要。

     

    在Derived::mf4函数中,当编译器看到这里使用名称mf2,必须估算它refer to什么东西。编译器首先查找local作用域(也就是mf4覆盖的作用域),在那儿没找到任何东西名为mf2。于是查找其外围作用域,也就是class Derived覆盖的作用域。还是没找到任何东西名为mf2,于是再往外围移动,本例为base class。在那儿编译器找到一个名为mf2的东西了,于是停止查找。如果Base内还是没有mf2,查找动作便继续下去,首先找内含Base的那个namespace(s)的作用域(如果有的话),最后往global作用域找去。

     

    再次考虑上面的例子,这次让我们重载base中的mf1和mf3,并且添加一个新版mf3到Derived去:

    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();
    };

    现在,base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承!

    Derived d;
    int x;
    
    d.mf1();                   // fine, calls Derived::mf1
    d.mf1(x);                  // error! Derived::mf1 hides Base::mf1
    d.mf2();                   // fine, calls Base::mf2
    
    d.mf3();                   // fine, calls Derived::mf3
    d.mf3(x);                  // error! Derived::mf3 hides Base::mf3

    如你所见,即使base classes和derived classes内的函数有不同的参数类型,而且不论函数是virtual或non-virtual,都会发生名称遮蔽。这和本条款一开始展示的道理相同,如今Derived内的函数mf3遮掩了一个名为mf3但类型不同的base函数。

     

    不幸的是你通常会想继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就就违反了base和derived classes之间的is-a关系。可以使用using声明式达成目标:

    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;       // make all things in Base named mf1 and mf3
      using Base::mf3;       // visible (and public) in Derived's scope
    
      virtual void mf1();
      void mf3();
      void mf4();
    };

    现在,继承机制将一如往昔地运作:

    Derived d;
    int x;
    
    d.mf1();                 // still fine, still calls Derived::mf1
    d.mf1(x);                // now okay, calls Base::mf1
    
    d.mf2();                 // still fine, still calls Base::mf2
    
    d.mf3();                 // fine, calls Derived::mf3
    d.mf3(x);                // now okay, calls Base::mf3

    有时候你并不想继承base classes的所有函数,这是可以理解的。但是在public继承下,这绝对不可能发生,因为它违反了public继承所暗示的“base和derived classes之间的is-a关系”。这也就是为什么上述using声明式被放在derived class的public区域的原因:base class内的public名称在publicly derived class内也应该是public。

    然而在private继承之下它却可能是有意义的。假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。我们需要不同的技术,即一个简单的forwarding函数:

    class Base {
    public:
      virtual void mf1() = 0;
      virtual void mf1(int);
      ...                                    // as before
    };
    
    class Derived: private Base {
    public:
      virtual void mf1()                   // forwarding function
      { Base::mf1(); }
      ...
    };
    
    Derived d;
    int x;
    
    d.mf1();                               // fine, calls Derived::mf1
    d.mf1(x);                              // error! Base::mf1() is hidden

     

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

    表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。身为class设计者,有时候你会希望derived classes只继承成员函数的接口(也就是声明);有时候你又会希望derived classes同时继承函数的接口和默认实现,但又希望它能够覆写(override)它们所继承的实现;有时候你希望derived classes同时继承函数的接口和实现,并且不允许覆写任何东西。

    考虑下面的代码:

    class Shape {
    public:
      virtual void draw() const = 0;
      virtual void error(const std::string& msg);
      int objectID() const;
    };
    
    class Rectangle: public Shape { ... };
    class Ellipse: public Shape { ... };

    Shape类中声明了三个函数:第一个是纯虚函数draw,它使得Shape成为了一个抽象类,所以客户不能够创建Shape类的实体,只能创建其derived classes的实体,而且derived classes中必须实现自己的draw函数(否则会报编译错误);第二个是虚函数error;第三个是普通函数objectID;

     1:声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

    Shape::draw函数是个纯虚函数,因为所有Shape对象都应该是可绘出的,但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法迥异于矩形绘法。Shape::draw的声明乃是对具象derived classes设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”

    在C++中,可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码。

     

    2:声明(非纯)虚函数的目的,是让derived classes继承该函数的接口和缺省实现。

    Shape::error函数是个虚函数,它表示每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉derived classes的设计者,“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。

     

    3:声明普通非虚函数的目的是为了令derived classes继承函数的接口及一份强制实现。

    如果成员函数是个非虚函数,意味是它并不打算在derived classes中有不同的行为。实际上一个非虚成员函数所表现的不变性(invariant)凌驾其特异性(specialization ),因为它表示不论derived class变得多么特异化,它的行为都不可以改变,所以它绝不该在derived class中被重新定义。

    Shape::objectID函数是个非虚函数,它的声明表示:“每个Shape对象都有一个用来产生对象识别码的函数;此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。

     

     

    35:考虑virtual函数以外的选择

             下面的代码中,GameCharacter表示游戏中的人物角色,成员函数healthValue表示人物的健康程度:

    class GameCharacter {
    public:
      virtual int healthValue() const;
      ... 
    };

             由于不同的人物可能以不同的方式计算他们的健康指数,因此将healthValue声明为virtual似乎是再明白不过的做法,该函数并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法。

     

             下面是几种不使用virtual的替代方法:

             1:由Non-Virtual interface手法实现Template Method模式

             该方法主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthVaiue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:

    class GameCharacter 
    {
    public:
      int healthValue() const 
      { 
        printf("begin of healthValue
    ");
        int retVal = doHealthValue();  
        printf("end of healthValue
    ");
        return retVal;
      }
    private:
      virtual int doHealthValue() const 
      {
        printf("this is GameCharacter::doHealthValue
    ");
      }   
    };
    
    class GCA : public GameCharacter
    {
    private:
        virtual int doHealthValue() const 
        {
            printf("this is GCA::doHealthValue
    ");
        }
    };
    
    int main()
    {
        GameCharacter gc;
        GCA gca;
    
        gc.healthValue();
        gca.healthValue();
    
        GameCharacter *pgc1 = new GameCharacter;
        GameCharacter *pgc2 = new GCA;
    
        pgc1->healthValue();
        pgc2->healthValue();
    
        delete pgc1;
        delete pgc2;
    }

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

    NVI手法的一个优点隐身在“做一些事前工作”和“做一些事后工作”之中。也就是确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。上述代码的结果如下:

    begin of healthValue
    this is GameCharacter::doHealthValue
    end of healthValue
    begin of healthValue
    this is GCA::doHealthValue
    end of healthValue
    begin of healthValue
    this is GameCharacter::doHealthValue
    end of healthValue
    begin of healthValue
    this is GCA::doHealthValue
    end of healthValue

     2:由Function Pointers实现Strategy模式

    另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

    class GameCharacter; // forward declaration
    
    // function for the default health calculation algorithm
    int defaultHealthCalc(const GameCharacter& gc);
    
    class GameCharacter {
    public:
      typedef int (*HealthCalcFunc)(const GameCharacter&);
    
      explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
      : healthFunc(hcf)
      {}
    
      int healthValue() const
      { return healthFunc(*this); }
    
    private:
      HealthCalcFunc healthFunc;
    };

    这个做法是常见的Strategy设计模式的简单应用。使用这种方法,同一人物类型之不同实体可以有不同的健康计算函数,而且某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

     

    3:由std::function完成Strategy模式

    基于函数指针的做法有些苛刻而死板:为什么要求“健康指数之计算”必须是个函数,而不能是某种“像函数的东西”呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢?

    可以改用一个类型为std::function的对象,这些约束就全都不见了。这样的对象可持有任何可调用物,比如函数指针、函数对象、或成员函数指针等:

    class GameCharacter; 
    int defaultHealthCalc(const GameCharacter& gc); 
    
    class GameCharacter {
    public:
       // HealthCalcFunc is any callable entity that can be called with
       // anything compatible with a GameCharacter and that returns anything
       // compatible with an int; see below for details
       typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
       
       explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
       : healthFunc(hcf)
       {}
    
       int healthValue() const
       { return healthFunc(*this);   }
    
    private:
      HealthCalcFunc healthFunc;
    };

             HealthCalcFunc是一种std::function类型,这种类型的对象可以持有任何与其签名函数兼容的可调用物。其签名函数“接受一个reference指向const GameCharacter,并返回int"。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int:

    short calcHealth(const GameCharacter&); //返回short而非int
    
    struct HealthCalculator {                  //函数对象
      int operator()(const GameCharacter&) const 
      { ... } 
    };
    
    class GameLevel {
    public:
      float health(const GameCharacter&) const;  //成员函数,返回float
      ... 
    }; 
    
    class EvilBadGuy: public GameCharacter { 
      ...
    };
    class EyeCandyCharacter: public GameCharacter {  
      ...  
    }; 
    
    EvilBadGuy ebg1(calcHealth);   //使用函数
    
    EyeCandyCharacter ecc1(HealthCalculator());    //使用函数对象
    
    GameLevel currentLevel;           //使用成员函数
    EvilBadGuy ebg2(
      std::bind(&GameLevel::health, currentLevel, _1)
    );

              4:传统的Strategy模式

             传统的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:

     

             这张图表示GameCharacter是某个继承体系的根类,体系中的EvilBadGuy和EyeCandyCharacter都是derived classes;HealthCalcFunc是另一个继承体系的根类,体系中的S1owHealthLoser和FastHealthLoser都是derived classes,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcF}nc继承体系的对象。具体的代码如下:

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

     

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

    之前的条款说过,所谓public继承意味is-a的关系;在class内声明一个non-virtual函数会为该class建立起一个不变性,凌驾其特异性。如果将这两个观点施行于两个classes:B(ase)和D(erived)以及non-virtual成员函数B::mf身上,意味着:适用于B对象的每一件事,也适用于D对象;B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。

    如果D重新定义mf,这样便出现矛盾:如果D真有必要实现出与B不同的mf,那么“每个D都是一个B”就不为真。既然如此D就不该以public形式继承B。另一方面,如果D真的必须以public方式继承B,并且如果D真有需要实现出与B不同的mf,那么mf就无法为B反映出“不变性凌驾特异性”的性质。既然这样mf应该声明为virtual函数。最后,如果每个D真的是一个B,并且如果mf真的为B反映出“不变性凌驾特异性”的性质,那么D便不需要重新定义mf,而且它也不应该尝试这样做。

        因此:任何情况下都不该重新定义一个继承而来的non-virtual函数。

     

     

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

    virtual函数系动态绑定,而缺省参数值却是静态绑定。

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

     

    38:通过复合塑模出has-a或“根据某物实现出”(is-implemented-in-terms-of)

    复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:

    class Address { ... }; 
    class PhoneNumber { ... };
    
    class Person {
    public:
      ...
    private:
      std::string name;               // composed object
      Address address;                // ditto
      PhoneNumber voiceNumber;        // ditto
      PhoneNumber faxNumber;          // ditto
    };

    上面的代码中,Person对象由string,Address,PhoneNumber构成。

     1:复合有两个意义:has-a(有一个),或这是is-implemented-in-terms-of(根据某物实现出)。

    因为你正打算在你的软件中处理两个不同的领域。如果程序中的对象相当于世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,像是缓冲区、互斥锁、查找树等等。这些对象相当于软件的实现域。

    当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

     

    2:has-a的关系很好区分,比较麻烦的是区分is-a和is-implemented-in-terms-of这两种对象关系。

    比如:某些情况下必须自己实现一个sets而不能使用标准库提供的版本。实现sets的方法很多,其中一种便是在底层采用标准库的linked lists。

    首先想到让set<T>继承list<T>:

    template<typename T>                       // the wrong way to use list for Set
    class Set: public std::list<T> { ... };

    这是错误的,因为public继承意味着is-a的关系,如果D是一种B,对B为真的每一件事情对D也都应该为真。但list可以内含重复元素,但是set的定义却不允许包含重复元素。因此“Set是一种list”并不为真。

    正确的做法是,Set对象可根据一个list对象实现出来:

    template<class T>                   // the right way to use list for Set
    class Set {
    public:
    bool member(const T& item) const;
      void insert(const T& item);
      ...
    private:
      std::list<T> rep;                 // representation for Set data
    };
    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);
    }

     

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

             1:如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象:

    class Person { ... };
    class Student: private Person { ... };     // inheritance is now private
    
    void eat(const Person& p);                 // anyone can eat
    
    Person p;                                  // p is a Person
    Student s;                                 // s is a Student
    eat(s);                                    // error! a Student isn't a Person

             上面针对s的eat调用将会报错,当eat的形参是Person或Person*时也一样,都会报错:error: ‘Person’ is an inaccessible base of ‘Student’。

    由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。

              2:Private继承意味着implemented-in-terms-of(根据某物实现出)。如果让class D以private形式继承class B,你的用意是为了采用class B内已经具备的某些特性,不是因为B对象和D对象存在有任何观念上的关系。因此,private继承纯粹只是一种实现技术,如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。

     

             3:Private继承意味is-implemented-in-terms-of,之前的条款指出复合的意义也是这样。如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。

            

             4:为了能够知道Widget成员函数的调用频率,需要记录每个成员函数的调用次数,然后周期性的审查这些信息。为了完整这个工作,需要设定一个定时器,周期性的取出Widget的状态。

             假设当前有一个定时器类:

    class Timer {
    public:
      virtual void onTick() const;          // automatically called for each tick
      ...
    };

             onTick函数会周期性的执行。因此,可以重新定义那个onTick函数,让其取出Widget当前状态。

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

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

            藉由private继承,Timer的public OnTick在Widget内变成private了。

    5:上面的方法不是唯一实现目的的方法,其实可以使用复合:

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

     使用复合要比使用private继承有更多的优势:首先,你或许会想设计Widget使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived classes将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。

    6:private继承主要用于“当一个意欲成为derived class者想访问base class的protected成分,或为了重新定义一或多个virtual函数”,但这时候两个classes之间的概念关系其实是is-implemented-in-terms-of而非is-a。

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

    7:private继承还适用于一种比较激进的情况:如果一个类不带任何数据,也就是它没有non-static成员变量,没有virtual函数(因为这种函数会为每个对象带来一个vptr),也没有virtual base classes(这样的base classes也会导致体积的额外开销)。这种类的对象不使用任何空间,因为没有隶属于对象的数据需要存储。但是C++规定,凡是独立(非附属)的对象必须有非0大小,所以:

    class Empty 
    {
    public:
        fun() {printf("this is fun");}
    private:
        fun2() {printf("this is fun2");}
    };                      
    
    class HoldsAnInt {  
    private:
      int x;
      Empty e;   
    };

      上面的类定义,sizeof(HoldsAnInt)会大于sizeof(int),测试结果是sizeof(Empty)为1, sizeof(int)为4,而sizeof(HoldsAnInt)为8。因为面对大小为0的独立非附属对象,C++要求默默插入一个char到空对象中,然而因为内存对其的需求,所以sizeof(HoldsAnInt)为8。

     上面的情况适用于独立非附属对象,但是不适用于derived class内的base class成分,因为它不是独立非附属的,因此:

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

    这样的定义,sizeof(HoldsAnInt)等于sizeof(int),这就是所谓的EBO(empty base optimization;空白基类最优化),EBO一般只在单一继承可行。

    注意,上面的空类不是真的empty,它可以包含typedefs, enums, static成员变量或non-virtual函数。

     

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

     1:多重继承情况下,派生类可能从多个base class继承相同的名称,从而导致歧义:

    class BorrowableItem {  
    public:
      void checkOut();   
    };
    
    class ElectronicGadget {
    private:
      bool checkOut(int a) const;  
    };
    
    class MP3Player:                 
      public BorrowableItem,         
      public ElectronicGadget
    { };                       
    
    MP3Player mp;
    mp.checkOut();                     // ambiguous! which checkOut?

     上面的代码对checkOut的调用会报错:” reference to ‘checkOut’ is ambiguous”,及时两个候选函数的访问权限不同,参数也不相同。为了解决歧义,必须明确指出要调用哪一个base class内的函数:mp.BorrowableItem::checkOut()

     

             2:多重继承的情况下,有可能形成“钻石型多重继承”的情况。为了避免某个数据发生多份拷贝的情况,必须使那些带有此数据的class成为一个virtual base class:

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

             这种方法的缺点是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。

    另外,virtual base的初始化责任是由继承体系中的最低层(most derived) class负责,这表示:(1)classes若派生自virtual bases而需要初始化,必须认知其virtual bases--不论那些bases距离多远;(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

    我对virtual base classes(亦相当于对virtual继承)的忠告很简单。第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。

    下面是一种多重继承的合理应用场景:

    class IPerson {
    public:
      virtual ~IPerson();
    
      virtual std::string name() const = 0;
      virtual std::string birthDate() const = 0;
    };

    IPerson是个Interface class,CPerson是要继承该类并需要提供继承自IPerson的pure virtual函数的实现代码。现在有个现成的类PersonInfo,它可以完成CPerson所需要的实际工作:

    class PersonInfo {
    public:
      explicit PersonInfo(DatabaseID pid);
      virtual ~PersonInfo();
    
      virtual const char * theName() const;
      virtual const char * theBirthDate() const;
    
    private:
      virtual const char * valueDelimOpen() const;      // see
      virtual const char * valueDelimClose() const;     // below
    };

    PersonInfo用于以各种格式打印数据库字段,每个字段值的起始字符和终止字符由valueDelimOpen和valueDelimClose返回,默认的实现分别是’[’ 和 ’]’,但是这两个界限符号并非人人喜欢,因此valueDelimOpen和valueDelimClose是virtual函数,允许派生类设置自己的界限符号。所以,PersonInfo::theName的实现可能如下:

    const char * PersonInfo::valueDelimOpen() const
    {
      return "[";                       // default opening delimiter
    }
    
    const char * PersonInfo::valueDelimClose() const
    {
      return "]";                       // default closing delimiter
    }
    
    const char * PersonInfo::theName() const
    {
      static char value[Max_Formatted_Field_Value_Length];
    
      // write opening delimiter
      std::strcpy(value, valueDelimOpen());
    
      //append to the string in value this object's   name field (being careful
      //to avoid buffer overruns!)
    
      // write closing delimiter
      std::strcat(value, valueDelimClose());
    
      return value;
    }

    作为CPerson的实现者,发现可以使用PersonInfo实现name和birthDate,但是需要界限符号为空。因此,CPerson和PersonInfo的关系是is-implemented-in-terms-of,我们知道这种关系可以有两种技术实现:复合和private继承。条款39指出复合通常是较受欢迎的做法,但如果需要重新定义virtual函数,那么继承是必要的。本例之中CPerson需要重新定义valueDelimOpen和valueDelimClose,所以单纯的复合无法应付。最直接的解法就是令CPerson以private形式继承PersonInfo。

    CPerson也必须实现IPerson接口,因此需要以public继承IPerson。因此这就是多重继承的一个通情达理的应用:

    class CPerson: public IPerson, private PersonInfo {     // note use of MI
    public:
      explicit CPerson(    DatabaseID pid): PersonInfo(pid) {}
      virtual std::string name() const                     
      { return PersonInfo::theName(); }                    
                                                   
      virtual std::string birthDate() const              
      { return PersonInfo::theBirthDate(); }
    private:                                               
      const char * valueDelimOpen() const { return ""; }    
      const char * valueDelimClose() const { return ""; }  
    };  

    最后,需要注意的是,如果某种需求下,你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想--几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。只要确定,你的确是在明智而审慎的情况下使用它。

  • 相关阅读:
    android handle详解
    android面试详解
    linux网络编程-一个简单的线程池(41)
    linux网络编程-posix条件变量(40)
    如何写一个简单的分页
    jQuery 3 有哪些新东西
    浅析正则表达式模式匹配的 String 方法
    jQuery源码浅析2–奇技淫巧
    前端文本截断
    你会用setTimeout吗
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7849533.html
Copyright © 2020-2023  润新知