0%

Effective C++之继承与面向对象设计

6章:继承与面向对象设计

条款32:确定你的public继承塑模出is-a关系
  • public继承”意味is-a。适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象
条款33:避免遮掩继承而来的名称
  • derived class的作用域被嵌套在base class作用域内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    class Base
    {
    private:
    int x;
    public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    void mf2();
    void mf2(double);
    };

    class Derived : public Base
    {
    public:
    virtual void mf1();
    void mf2();
    };

    Derived d;
    int x;
    ...
    d.mf1(); // 没问题, 调用 Derived::mf1
    d.mf1(x); // 错误! 因为 Derived::mf1 遮掩了 Base::mf1
    d.mf2(); // 没问题, 调用 Derived::mf2
    d.mf2(x); // 错误! 因为 Derived::mf2 遮掩了 Base::mf2

    这段代码中base class内所有名为mf1mf2的函数都被derived class内的mf1mf2函数遮掩掉了。从名称查找观点来看,Base::mf1Base::mf2不再被Derived继承!

    实际上如果你正在使用public继承而又不继承那些重载函数,就是违反basederived class之间的is-a关系,而条款32说过is-apublic继承的基石。因此你几乎总会想要推翻C++对“继承而来的名称”的缺省遮掩行为。

    • 使用using声明式

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      class Base
      {
      private:
      int x;

      public:
      virtual void mf1() = 0;
      virtual void mf1(int);
      void mf2();
      void mf2(double);
      ...
      };

      class Derived : public Base
      {
      public:
      using Base::mf1; // 让 Base class 内名为 mf1 和 mf2 的所有东西
      using Base::mf2; // 在 Derived 作用域内都可见, 并且 public
      virtual void mf1();
      void mf2();
      };

      Derived d;
      int x;
      ...
      d.mf1(); // 没问题, 调用 Derived::mf1
      d.mf1(x); // 现在没问题了, 调用 Based::mf1
      d.mf2(); // 没问题, 调用 Derived::mf2
      d.mf2(x); // 现在没问题了, 调用 Based::mf2
    • 使用inline转交函数(forwarding function

      然而在private继承之下(见条款39),有时候你并不想继承base class的所有函数。假设Derivedprivate形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      class Base
      {
      public:
      virtual void mf1() = 0;
      virtual void mf1(int);
      ... // 与前同
      };

      class Derived : private Base
      {
      public:
      virtual void mf1() // 转交函数
      {
      Base::mf1(); // 暗自成为 inline (见条款 30)
      }
      ...
      };

      Derived d;
      int x;

      d.mf1(); // 很好, 调用的是 Derived::mf1
      d.mf1(x); // 错误! Base::mf1 被遮掩
条款34:区分接口继承和实现继承

身为class设计者,有时候你会希望derived class只继承成员函数的接口,也就是声明;有时候你又会希望derived class同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;又有时候你希望derived class同时继承函数的接口和实现,并且不允许覆写任何东西。

base类强烈影响以public形式继承它的derived class,因为:

  • 成员函数的接口总是会被继承

  • 声明一个pure virtual函数的目的是为了让derived类只继承函数接口

    含有pure virtual函数的类属于抽象基类,不能被实例化。继承了它们的类必须重新声明此函数接口并给予实现。抽象基类可以给pure virtual函数提供定义,但调用它的唯一途径是“调用时指定其class的名称”。

  • 声明impure函数的目的是让derived class继承该函数的接口和缺省实现

    但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。欲探讨原因,让我们考虑XYZ航空公司设计的飞机继承体系。该公司只有A型和B型两种飞机,两者都以相同方式飞行。因此XYZ设计出这样的继承体系:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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 {...};

    为了表示所有飞机都一定能飞,并阐明“不同型飞机原则上需要不同的fly实现”,Airplane::fly被声明为virtual。然而为了避免在ModelAModelB中撰写相同代码,缺省飞行行为由Airplane::fly提供,它同时被ModelAModelB继承。

    现在,XYZ航空公司决定购买一种新式C型飞机。C型和A型以及B型的飞行方式不同。XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,竟忘了重新定义其fly函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class ModelC : public Airplane
    {
    // 未声明fly函数
    };

    Airport PDX(... ); // PDX 是我家附近的机场
    Airplane* pa= new ModelC;
    ...
    // 这将酿成大灾难
    pa->fly(PDX); // 调用 Airplane::fly

    一种解救办法为切断“virtual函数接口”和其“缺省实现”之间的连接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    class Airplane
    {
    public:
    // 现在是 pure virtual 函数
    virtual void fly(const Airport& destination) = 0;
    protected:
    void defaultFly (const Airport& destination);
    };

    void Airplane::defaultFly(const Airport& 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);
    }
    };

    // 现在 ModelC class 不可能意外继承不正确的 fly 实现代码了
    // 因为 Airplane 中的 pure virtual 函数追使 ModelC 必须提供自己的 f1y 版本
    class ModelC: public Airplane
    {
    public:
    virtual void fly(const Airport& destination);
    };

    void ModelC::fly(const Airport& destination)
    {
    ... // 将 C 型飞机飞至指定的目的地
    }

    另一种解救方法是,利用“pure virtual函数必须在derived class中重新声明,但它们也可以拥有自己的实现”这一事实。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    class Airplane
    {
    public:
    // 现在是 pure virtual 函数
    virtual void fly(const Airport& destination) = 0;
    };

    // 给予 pure virtual 函数实现
    void Airplane::fly(const Airport& destination)
    {
    ... // 缺省行为, 将飞机飞至指定的目的地
    }

    class ModelA : public Airplane
    {
    public:
    virtual void fly(const Airport& destination)
    {
    Airplane::fly(destination); // 通过指定其类名调用它
    }
    ...
    };

    class ModelB : public Airplane
    {
    public:
    virtuai void fly(const Airport& destination)
    {
    Airplane::fly(destination); // 通过指定其类名调用它
    }
    ...
    };

    class ModelC : public Airplane
    {
    public:
    virtual void fly(const Airport& destination);
    };

    void ModelC::fly(const Airport& destination)
    {
    ... // 将 C 型飞机飞至指定的目的地
    }
  • 声明non-virtual函数的目的是为了derived class继承函数的接口及其强制性实现

    non-virtual函数为其所属class建立的意义是不变性凌驾其特异性,所以它不该在derived class中被重新定义。

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

假设你正在写一个视频游戏软件,你的游戏属于暴力砍杀类型,剧中人物存在被伤害而降低健康状态的情况。因此你决定提供一个成员函数 healthvalue,它会返回一个整数,表示人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将healthvalue声明为virtual似乎是再明白不过的做法:

1
2
3
4
5
6
class GameCharacter
{
public:
virtual int healthvalue() const; // 返回人物的健康指数,
// derived classes 可重新定义它
};

现在考虑一些其它解法:

  • 借助non-virtual interface(NVI)手法实现Template Method模式

    保留healthvaluepublic成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class GameCharacter
    {
    public:
    int healthvalue() const // derived class 不重新定义它
    {
    ... // 做一些事前工作
    int retval = doHealthvalue(); // 做真正的工作
    ... // 做一些事后工作
    return retval;
    }

    // NVI 手法下没必要让 virtual 函数一定是 private
    private:
    virtual int doHealthValue() const // derived class 可重新定义它
    {
    ... // 缺省算法, 计算健康指数。
    }
    };

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

    NVI手法的一个优点隐藏在上述代码注释“做一些事前工作”和“做一些事后工作”之中。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证 class约束条件、验证函数先决条件等。“事后工作”可以包括互斥器解除锁定、验证函数的事后条件、再次验证class约束条件等。如果你让客户直接调用virtual函数,就没有任何好办法可以做这些事。

  • 借助Function Pointer实现Strategy模式

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class GameCharacter; // 前向声明
    // 以下函数是计算健康指数的缺省算法
    int defaultHealthCalc(const GameCharacter& gc);

    class GameCharacter
    {
    public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);

    explicit GameCharacter (HealthCalcFunc hcf = defaultFealthCalc)
    : healthFunc(hcf) {}

    int healthvalue() const
    {
    return healthFunc (*this);
    }
    ...

    private:
    HealthCalcFunc healthFunc;
    };

    这种设计策略和前面介绍的方法相比提供了一些有趣的弹性:

    • 同一人物类型的不同实体可以有不同的健康计算函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class EvilBadGuy : public GameCharacter
      {
      public:
      explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
      : GameCharacter(hcf) {...}
      ...
      };

      int loseHealthQuickly(const GameCharacter&); // 健康指数计算函数 1
      int loseHealthSlowly(const GameCharacter&); // 健康指数计算函数 2
      EvilBadGuy ebg1(loseHealthQuickly); // 相同类型的人物搭配
      EvilBadGuy ebg2(loseHealthSlowly); // 不同的健康计算方式
    • 某已知人物之健康指数计算函数可在运行期变更

      例如GameCharacter可提供一个成员函数setHealthcalculator,用来替换当前的健康指数计算函数。

      然而这种策略意味着,计算函数并不能访问“被计算健康指数的”那个对象的内部(non-public)成分。如果需要访问non-public成分进行精确计算,这就有问题了。

      唯一能够解决的办法就是:弱化class的封装。例如,将健康计算函数声明为friend。或是为其实现的某一部分提供public访问函数。这需要你来权衡利弊进行抉择。

  • 借助std::function实现Strategy模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class GameCharacter;
    int defaultHealthCalc(const GameCharacter& gc);

    class GameCharacter
    {
    public:
    // 不同之处
    typedef std::function<int (const GameCharacter&)> HealthCalcFunc;

    explicit GameCharacter (HealthCalcFunc hcf = defaultFealthCalc)
    : healthFunc(hcf) {}

    int healthvalue() const
    {
    return healthFunc (*this);
    }
    ...

    private:
    HealthCalcFunc healthFunc;
    };

    那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”。这个std::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换const GameCharacter&,而其返回类型可被隐式转换int

    这就提供了很大的灵活性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    short calcHealth(const GameCharacter&);	// 健康计算函数
    // 注意其返回类型为 non-int
    struct HealthCalculator
    {
    // 为计算健康而设计的函数对象
    int operator()(const GameCharacter&) const
    {...}
    };

    class GameLevel
    {
    public:
    float health(const GameCharacter&) const; // 成员函数, 用以计算健康
    // 注意其 non-int 返回类型
    };

    // 人物类型 1
    class EvilBadGuy: public GameCharacter
    {
    ... // 同前
    };

    // 人物类型 2
    class EyeCandyCharacter: public GameCharacter
    {
    // 假设其构造函数与 EvilBadGuy 同
    };

    // 人物 1, 使用某个函数计算健康指数
    EvilBadGuy ebg1(calcHealth);

    // 人物 2, 使用某个函数对象计算健康指数
    EyeCandyCharacter ecc1(HealthCalculator());

    GameLevel currentLevel;
    ...
    // 人物 3, 使用某个成员函数计算健康指数
    EvilBadGuy ebg2(std::bind(&GameLevel::health, &currentLevel, _1));
  • 传统的Strategy模式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class GameCharacter;

    class HealthCalcFunc
    {
    public:
    virtual ~HealthCalcFunc() {}
    virtual int calc(const GameCharacter& gc) const = 0;
    };

    class SlowHealthLoser : HealthCalcFunc
    {
    public:
    virtual int calc(const GameCharacter& gc) const
    {
    ...
    }
    ...
    };

    class FastHealthLoser : HealthCalcFunc
    {
    public:
    virtual int calc(const GameCharacter& gc) const
    {
    ...
    }
    ...
    };

    class GameCharacter
    {
    public:
    explicit GameCharacter(HealthCalcFunc* phcf) : pHealthCalc(phcf) {}

    int healthValue() const
    {
    return pHealthCalc->calc(*this);
    }

    private:
    HealthCalcFunc* pHealthCalc;
    };
条款36:绝不重新定义继承而来的non-virtual函数
  • 如题
条款37:绝不重新定义继承而来的缺省参数值

本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”:virtual函数是动态绑定(后期绑定,延迟绑定),而缺省参数值却是静态绑定(前期绑定,早绑定)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 一个用以描述几何形状的class
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 继承缺省参数值
};
  • 对象的静态类型就是它在程序中所声明的类型,对象的动态类型则是指“目前所指对象的类型”

    1
    2
    3
    shape* ps; // 静态类型为 shape*, 无动态类型
    Shape* pc = new Circle; // 静态类型为 Shape*, 动态类型是 Circle*
    Shape* pr = new Rectangle; // 静态类型为 shape*, 动态类型是 Rectangle*

    动态类型可在程序执行过程中改变(通常是经由赋值动作):

    1
    2
    ps = pc; // ps 的动态类型如今是 circle*
    ps = pr; // ps 的动态类型如今是 Rectangle*

virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。

1
2
pr->draw(); // 调用的是 Rectangle::draw(shape::Red)!
// 注意缺省参数是 Red 而不是 Green

此例之中,pr的动态类型是Rectangle*,所以调用的是Rectanglevirtual函数。Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!这不符合预期。

解救方法是利用条款35中介绍的NVI手法替代virtual函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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; // 注意, 不须指定缺省参数值
...
};

这个设计很清楚地使得draw函数的color缺省参数值总是Red

条款38:通过复合塑模出has-ais-implemented-in-terms-of

当某种类型的对象内含其它对象时便形成了复合关系。

  • 应用域

    程序中的对象其实相当于你所塑造出来的世界中的某些事物。比如人,汽车,高楼大厦等。这样的对象属于应用域部分。

  • 实现域

    其它对象如缓冲区,互斥器,搜索树等纯粹是实现细节上的工具。这些对象相当于软件中的实现域。

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

  • 复合的意义和public继承完全不同

    注意区分is-ais-implemented-in-terms-of这两种对象关系。

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

一个derived类继承base类有publicprotectedprivate三种继承方式。

  • 公有继承(public)(普遍使用)

    base类的public成员也是derived类的public成员,base类的protected成员也是derived类的protected成员

  • 保护继承(protected)(基本不使用)

    base类的publicprotected成员将成为derived类的protected成员。

  • 私有继承(private)(少的情况使用)

    base类的publicprotected成员将成为derived类的private成员。

无论哪一种继承方式,base类的private成员都不能直接被derived类访问,但是可以通过调用base类的publicprotected成员间接来访问(如果base类提供了访问接口的话)。

  • private继承意味着is-implemented-in-terms-of

    private继承只限于软件实现层面,这就是为什么private继承后,base class的所有成分(除了private)在你的class内都是private,因为它们都只是实现细节而已。

  • 尽可能使用复合,必要时才使用private继承

    1. derived类想要访问base classprotected成员时

    2. derived类为了重新定义base classvirtual函数时

      现在考虑一个Widget类,它需要用到另一个计时器Timer类来实现一些业务。

      1
      2
      3
      4
      5
      6
          class Timer
      {
      public:
      explicit Timer(int tickFrequecy);
      virtual void onTick() const; // 定时器每滴答一次, 此函数就调用一次
      };

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

      1
      2
      3
      4
      5
      6
      class Widget
      {
      ...
      private:
      virtual void onTick() const; //
      };

      通过private继承,Timerpublic onTick函数在Widget内变成private,而我们重新声明(定义)时仍然把它留在那儿。如果把onTick放进public内会误导客户以为他们可以调用它,那就违反了条款18

      另一种设计方案是采用复合 + public继承:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      class Widget
      {
      ...
      private:
      // 在 Widget 内声明一个嵌套式 private class
      class WidgetTimer : public Timer
      {
      public:
      // 以 public 方式继承并重新定义 virtual 函数
      virtual void onTick() const;
      ...
      };
      WidgetTimer timer;
      ...
      };

      采用第二种较为复杂的方式的两点理由:

      • 你或许会想设计Widget使它得以拥有derived class,但同时你可能会想阻止derived class重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimerWidget内部的一个private成员并继承自TimerWidgetderived class将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。
      • 你或许会想要将Widget的编译依存性降至最低(条款31)。如果Widget继承Timer,当Widget被编译时Timer的定义必须可见,所以定义Widget的那个文件恐怕必须#include Timer.h。但如果WidgetTimer移出Widget所在文件之外而Widget内含指针指向一个WidgetTimerWidget可以只带着一个简单的WidgetTimer前向声明式,不再需要#include任何与Timer有关的东西。很容易就实现了解耦。
    3. EBOempty base optimization

      empty class指的是没有non-static成员变量,没有virtual函数,也没有virtual base class。于是这种class不占用理论上不占用任何内存空间。然而在实现技术上,C++要求独立(非附属)对象都必须有非零大小。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class Empty { }; // 没有任何需要存储的数据

      // 理论上 sizeof(HoldAnInt) = 4 字节(就一个 int)
      // 实际上 sizeof(HoldAnInt) = 8 字节
      class HoldAnInt
      {
      private:
      int x; // 4 字节
      Empty e; // 理论上应该不占据内存
      };
      • C++编译器默认为empty class独立对象安插一个char1字节)

      • 由于内存对齐HoldAnInt的实际大小为4 + 1(内存对齐至 4) = 8

        但这个约束不适用于derived class对象的base class成分(非独立):

        1
        2
        3
        4
        5
        6
        // 理论和实际上 sizeof(HoldAnInt) = 4 字节(就一个 int)
        class HoldAnInt : public Empty
        {
        private:
        int x; // 4 字节
        };

        现实中的empty class并非真是empty。往往内含typedefenumstatic成员变量,或non-virtual函数。STL就有许多技术用途的empty class,其中内含有用的成员(通常是typedef),包括base class unary_functionbinary_function,这些是“用户自定义之函数对象”通常会继承的class。由于EBO,这样的继承很少增加derived class的大小。

条款40:明智而谨慎地使用多重继承
  • 多重继承较单一继承复杂,可能导致歧义性

    当继承的多个base具有同名成员函数时,derived类调用时会出现歧义。

  • 有时多重继承不得不需要virtual继承,而这会带来额外的体积、运行速度、初始化复杂度等成本

    比如这样的钻石继承体系:

    这个继承体系中某个base class和某个derived class之间有一条以上的相通路线,假设File class有个成员变量fileName,那么IOFile从每一个base class各继承一份,所以其对象内会有两份fileName成员变量。然而IOFile对象只该有一个文件名称,所以名称fileName不该重复。

    解决办法就是采用virtual继承:

    virtual继承带来的后果是:

    • 使用virtual继承的那些class所产生的对象往往比使用non-virtual继承的class体积大

    • 访问virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢

    • 另外,支配“virtual base class初始化”的规则比起non-virtual base class的情况远为复杂且不直观。virtual base class的初始化责任是由继承体系中的最低层(most derivedclass负责的,这意味着:

      1. class若派生自virtual base而需要初始化,必须知道其virtual base class——不论那些base class距离多远。
      2. 当一个新的derived class加入继承体系中,它必须承担其virtual base class的初始化责任。

      关于virtual继承的忠告:

    1. 非必要不使用virtual base。平常请使用non-virtual继承。
    2. 如果必须使用virtual base class,尽可能避免在其中放置数据。这么一来你就不需担心这些class身上的初始化(和赋值)所带来的诡异事情了。

最后看一个多重继承的应用例子——public继承某个Interface classprivate继承某个协助实现的class

(参考书籍P195-P198