• 【读书笔记】【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被遮掩了
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19

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

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

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

    • 如果有设计模式的基础会更容易理解该条款的内容。
    • 该条款主要通过一个继承体系来体现:
      class GameCharacter  {
      public:
            virtual int healthValue() const;
            ...
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 这个函数不是纯虚函数,所以这表明会有个缺省的实现(这就容易成为该类的弱点),所以最好找一个替换虚函数的方案,本节内容就是讨论能够替换 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  {    // 派生类可以重新定义它
                    ...    // 缺省算法,可以重新定义
              }
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
      • 优点在于:在它做真正的事情前后都可以加入一些其他东西,就像上面定义体现的那样;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;
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
      • 优点在于:同一人物类型不同实体可以有不同的健康计算函数;某个已知的人物健康指数计算函数可以在运行期变更。
      • 缺点在于:人物的健康根据该人物的 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,使用某个成员函数计算健康指数
        {};
        
        • 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
    • 方案四,古典的 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;
        };
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
      • 具体来说,传统实现就是将继承体系内的 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)
      
      • 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
    • 替换方法是 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;
      };
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16

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

    • 多重继承会比单一继承复杂。
      • 多重继承可能导致歧义,即程序可能从多个基类继承了相同的名称(函数、typedef 等);为了解决这种歧义,首先要明确调用哪个基类的函数,然后带上那个基类。
      • 菱形继承问题,即所继承的基类在它们体系中又有共同的基类;要想只有一份 most base 类的成分,就只能将中间的类虚拟继承自 most base 类,如代码所示:
        class File {  ...  };
        class InputFile: virtual public File {  ...  };
        class OutputFile: virtual public File {  ...  };
        class IOFile: public InputFile, public OutputFile
        {  ...  };
        
        • 1
        • 2
        • 3
        • 4
        • 5
    • virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,如果 virtual base classes 不带任何数据,将是最具实用价值的情况。
    • 多重继承的确有正当用途,其中一个情节涉及 public 继承某个 Interface class 和 private 继承某个协助实现的 class 的两相组合。
  • 相关阅读:
    大型通用ERP生产管理系统源码
    pytorch.数据结构Tensor
    Redis底层数据结构之IntSet
    猿创征文|风起YuKon(禹贡)-空间数据库结缘数字孪生和实景三维建设
    jwt_拦截器_校验token
    javascript 根据数组指定字段值,实现升序/降序
    什么是父子组件通信,一秒看懂emit
    赠书福利开始啦—〖Effective软件测试〗
    MindFusion.WinForms Pack 2022.R2
    如何在DBNet中加入新的主干网络
  • 原文地址:https://blog.csdn.net/weixin_44705592/article/details/126925329