【读书笔记】【Effective C++】继承与面向对象设计

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

  • public inheritance(公开继承)意味 is-a 的关系。【base 类的方法接口在 derived 类中都会有所体现】
    • 如果你令 class DDerived)以 public 形式继承 class BBase),你便是在告诉 C++ 编译器(亦或是看你代码的人):每一个类型为 D 的对象同时也是一个类型为 B 的对象,反之则不成立。
    • 在 C++ 领域中,任何函数如果期望获得一个类型为 B(或 pointer-to-B 或 reference-to-B)的实参,都也愿意接受一个 D 对象(或 pointer-to-D 或 reference-to-D)。
  • 如果 derived 类无法完全继承 base 类的所有方法,可以采取两种方案:
    • 第一种方案是在将调用错误限制在编译期,也就是直接不声明这个接口。【但其实就违背了本条款】
    • 第二种方案则是在运行期内提示错误,也就是在这个实现中报错即可。
  • is-a 并非是唯一存在于 class 之间的关系,另两个常见的关系是 has-a(有一个)和 is-implemented-in-terms-of(根据实物实现出)。

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

  • 本条款重点描述的是名字与作用域的关系,和继承权限(public、private)、虚机制均无关系。
  • 编译器对变量名称的匹配是由内向外的,一旦找出就停止搜索,然后再检查变量类型是否匹配正确。
  • derived classes 内的名称会遮掩 base classes 内的名称。
    • 基本理由是:防止程序员在程序库或应用框架内建立新的 derived class 时,附带地从疏远的 base class 中继承了重载函数。
  • 为了让 base class 中被遮掩的名称起作用,可以采用 using class 或转交函数。
  • using class 方案:
    • 在 public 继承中采取的方案。
    • 意思就是说使用 using 语句显式表明从 base class 中继承过来的函数名称。
    • 这样的方案会涵盖 base 类中一个名称可取得的所有重载版本。
  • 转交函数方案:
    • 有时候,并不想继承 base class 的所有函数。
    • 这时候就通过转交函数来获取特定的一个函数
      class Base  {
      public:
        virtual void mf1() = 0;
        virtual void mf1(int);
        ...
      };
      
      class Derived : private Base  {
      public:
        virtual void mf1()               // 转交函数,暗自成为inline(原因,见条款30)
        {  Base::mf1();  }
        ...
      };
      
      ...
      Derived d;
      int x;
      d.mf1();                           // 很好,调用的是Derived::mf1
      d.mf1(x);                         // 错误! Base::mf1被遮掩了
      

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

  • 接口继承和实现继承的区别就类似函数声明和函数定义之间的差异。
    • pure-virtual 只指定接口继承。
    • impure-virtual 指定接口继承和缺省的实现继承。
    • non-virtual 指定接口继承和强制性的实现继承。

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

  • 如果有设计模式的基础会更容易理解该条款的内容。
  • 该条款主要通过一个继承体系来体现:
    class GameCharacter  {
    public:
          virtual int healthValue() const;
          ...
    };
    
  • 这个函数不是纯虚函数,所以这表明会有个缺省的实现(这就容易成为该类的弱点),所以最好找一个替换虚函数的方案,本节内容就是讨论能够替换 virtual 函数的方案。
  • 方案一,借由 Non-Virtual Interface(NVI)手法实现 Template Method 模式:【调用留给 base 类,而真正的实现变成虚函数,允许 derived 类去重写】
    • 这个方法,主张 virtual 函数应该几乎总是 private。

    • 这个方法就是保留 healthValue 函数为 public,但让它成为 non-virtual,并间接调用一个 private virtual 函数:

      class GameCharacter  {
      public:
            int healthValue() const  {    // 派生类 不重新定义它
                  ...    // 做一些 其他工作
                  int retVal = doHealthValue();
                  ...
                  return retVal;
            }
        ...
      private:
            virtual int doHealthValue() const  {    // 派生类可以重新定义它
                  ...    // 缺省算法,可以重新定义
            }
      };
      
    • 优点在于:在它做真正的事情前后都可以加入一些其他东西,就像上面定义体现的那样;NVI 方法允许派生类重新定义 virtual 函数,决定如何去实现,但基类有着何时被调用的权利。

    • 缺点在于:这个方法让 virtual 函数是 private 的;有时候 derived 类的重写函数需要调用 base 类的对应虚函数,这时候就没有办法把虚函数设置为 private 的,也就是说这时候就没办法实施NVI方法。【降低了封装性】

  • 方案二,通过 Function Pointers 实现 Strategy 模式:【弱化 class 的封装】
    • 这个方法主张,健康指数的计算与人物类型无关,因此就不需要这个成分,完全可以让每个人物(每个 derived 类)通过一个函数来自己计算:
      class GameCharacter;
      int defaultHealthCalc(const GameCharacter& gc);
      class GameCharacter  {
      public:
          typdef int (*HealthCalcFunc) (const GameCharacter&);
          explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
          {}
          int healthValue() const
          {  return healthFunc(*this);  }
          ...
      private:
          HealthCalcFunc healthFunc;
      };
      
    • 优点在于:同一人物类型不同实体可以有不同的健康计算函数;某个已知的人物健康指数计算函数可以在运行期变更。
    • 缺点在于:人物的健康根据该人物的 public 接口得来的信息加以计算就没问题,但是要用到 non-public 信息计算,就会出问题。
  • 方案三,通过 tr1::function 完成 Strategy 模式:
    • 不用函数指针,而使用 tr1::function,这个对象可以保存任何可调用户:
      class GameCharacter;
      int defaultHealthCalc(const GameCharacter& gc);
      class GameCharacter  {
      public:
          typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
          explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
          {}
          int healthValue() const
          {  return healthFunc( *this);  }
          ...
      private:
          HealthCalcFunc healthFunc;
      };
      
      // HealthCalcFunc是个typedef,用来表现 tr1::function的某个实体,行为像一个函数指针。
      // 它的含义就是 接受一个指向GameCharacter的引用,并返回int。
      // 实现如下:
      short calcHealth(const GameCharacter&);
      struct HealthCalculator  {    // 为计算健康设计的成员对象
          int operator() (const GameCharacter&) const
          {  ...  }
      };
      class GameLevel  {
      public:
          float health(const GameCharacter&) const;    // 计算健康的成员函数
          ...
      };
      class EvilBadGuy: public GameCharacter  {
          ...        // 同前
      };
      class EveCandyCharacter: public GameCharacter  {
          ...        // 另一个人物类型,假设构造函数同EvilBadGuy
      };
      EvilBadGuy edg1(calcHealth);                // 人物1,使用某个函数计算健康指数
      EyeCandyCharacter ecc1(HealthCalculator());          // 人物2,使用某个函数对象计算健康指数
      GameLevel currentLevel;
      ...
      EvilBadGuy ebg2( std::tr1::bind(&GameLevel::health, currentLevel, _1)               // 人物3,使用某个成员函数计算健康指数
      {};
      
  • 方案四,古典的 Strategy 模式:
    • 设计模式中标准 Strategy 模式,代码如下:
      class GameCharacter;
      class HealthCalcFunc  {
      public:
        ...
        virtual int clac(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;
      };
      
    • 具体来说,传统实现就是将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。

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

  • 在派生类中,如果重新定义基类的函数,将会遮掩基类的相应函数。
    • 造成这样现象的原因是,non-virtual 函数都是静态绑定,如果重新定义,打断了 is-a 的关系。

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

  • 有了条款 36,可以知道本条款的讨论是限定在继承一个带有缺省参数值的 virtual 函数的场景下。
  • virtual 函数是动态绑定,而缺省参数值是静态绑定
  • 可能会在调用一个定义于 derived class 内的 virtual函数的同时,却使用 base class 为它所指定的缺省参数值。【C++ 这样做主要是节省执行效率】
    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;    // 注释①
        ...
    };
    
    Shape *ps;
    Shape *pc = new Circle;// pc的静态类型是Shape*,动态类型是Circle*
    Shape *pr = new Rectangle;// pr的静态类型是Shape*,动态类型是Rectangle*
    
    // 结果如下:
    pc->draw(ShapeColor::Red);// 相当于调用Circle::draw(ShapeColor::Red)
    pr->draw(ShapeColor::Red);// 相当于调用Rectangle::draw(ShapeColor::Red)
    pr->draw();// 也相当于调用Rectangle::draw(ShapeColor::Red)
    
  • 替换方法是 NVI 手法:
    • 让基类的 public non-virtual 函数调用 private virtual 函数,让 non-virtual 函数负责指定缺省参数值,virtual 函数负责实现具体的东西。

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

  • 复合,是类型之间的一种关系,当某种类型的对象内含它种类型对象,便是这种关系。【复合 = 内嵌 = 聚合 = 内含】
    • public 继承意味着 is-a 关系。
  • 复合意味着 has-a 或 is-implemented-in-terms-of:
    • 当发生于应用域内的对象之间,表现出 has-a 关系(一般就是 A 类的 private 中含有 B 类);【has-a 的意思是有一个,也就是内含】
    • 当发生于实现域内则表现 is-implemented-in-terms-of。【就好比如 queue 是由 deque 来实现的】【is-implemented-in-terms-of 的意思是根据某物实现出】
  • 区分:
    • 复合的意义与 public 继承完全不同。
    • is-implemented-in-trems-of 的实现方法不止有复合,还有 private 继承。

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

  • private 继承的首要规则:
    • 如果 class 之间的继承关系是 private,编译器不会自动将一个 derived class 对象转换为一个 base class 对象。【无法自动动态转换】
    • 由 private base class 继承而来的所有成员,在 derived class 都会变成 private 属性,纵使它们在 base class 中原本是 protected 或 public 属性。
  • private 继承意味 is-implemented-in-terms of(根据某物实现出),它通常比复合的级别低。【private 继承意味只有实现部分被继承,接口部分应略去;如果 D 以 private 形式继承 B,意思是 D 对象根据 B 对象实现而得,再没有其他含义了】
    • private 继承的使用场合是:当一个意欲成为 derived class 者想访问一个意欲成为 base class 者的 protected 成分;为了重新定义一个或多个 virtual 函数。
  • 和复合不同,private 继承可以造成 empty base 最优化,根据代码进行分析:
    // 第一种情况:
    // 在这种情况下sizeof(HoldsAnInt) = 8
    class Empty {};//没有数据,所以其对象应该不使用任何内存 
    class HoldsAnInt {
    private:
      int x;
      Empty e;// 对齐,没有优化,sizeof(Empty) = 1
    };
    
    // private继承的情况:
    // 在这种情况下sizeof(HoldsAnInt) = 4
    // 这就是所谓的空基类最优化(EBO)
    class HoldsAnInt :private Empty {
    private:
      int x;
    };
    

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

  • 多重继承会比单一继承复杂。
    • 多重继承可能导致歧义,即程序可能从多个基类继承了相同的名称(函数、typedef 等);为了解决这种歧义,首先要明确调用哪个基类的函数,然后带上那个基类。
    • 菱形继承问题,即所继承的基类在它们体系中又有共同的基类;要想只有一份 most base 类的成分,就只能将中间的类虚拟继承自 most base 类,如代码所示:
      class File {  ...  };
      class InputFile: virtual public File {  ...  };
      class OutputFile: virtual public File {  ...  };
      class IOFile: public InputFile, public OutputFile
      {  ...  };
      
  • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,如果 virtual base classes 不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途,其中一个情节涉及 public 继承某个 Interface class 和 private 继承某个协助实现的 class 的两相组合。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值