• 《Effective C++》简明笔记中


    32. 确定你的 public 继承模拟了 is-a 关系

    • 面向对象编程中最重要的一条。如果派生类D通过public方式继承了基类B,那么所用用于B的方法 或者 基类B自身具有的方法,都适用于D。

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

    • 如果派生类D通过public方式继承了基类B,那么D中的函数/变量名会遮掩B中的函数/变量名,如同局部作用域与全局作用域的关系一样。比如
      class B{
      public:
          void f(){}
          void f(int x){}
      };
      class D:public B{
      public:
          void f(){} // f不仅会覆盖B::f(),也会覆盖B::f(int),因为这是变量名覆盖
      };
      class E:public B{
      public:
          int f; // 即使f不是函数,也会覆盖B::f()和B::f(int)
      };

    34. 区别接口继承与实现继承

    • 接口继承,意味着继承方法的签名,包括返回类型,参数列表,方法名。
    • 实现继承,意味着继承方法的实现,即功能。
    • 基类中的纯虚函数意味着,派生类只继承接口,而自己进行实现。所有派生类都必须对基类的纯虚函数进行显式的继承(即使继承后仍然是个纯虚函数)。理论上,纯虚函数不必须实现(即只有声明没有定义),但也可以定义纯虚函数的函数体。如果定义了纯虚函数,那么调用该函数的唯一方法就是在调用时显式指定基类的名称。这使得我们有时候可以通过实现纯虚函数来进行某种缺省的实现。
      class Shape{
      public:
          virtual void draw() const =0 {};
          ...
      };
      class Circle:public Shape{
      public:
          void draw(){
              ... // 在隐喻的屏幕上绘制圆
          }
          ...
      };
      class InvisibleShape:public Shape{
      public:
          void draw(){
              Shape::draw(); // 对不可见的物体,调用缺省的纯虚函数实现
          }
          ...
      };
    • 基类中的非纯虚函数意味着,派生类需要同时集成接口和一份缺省实现。如果派生类中未声明该虚函数,就相当于自动继承了该函数,如果派生类自己实现了同样签名的函数,则使用自己的实现。使用非纯虚函数可能导致的一个风险,就是由于依赖于 “不去声明基类中的虚函数而自动获得继承”,而忘了该虚函数的存在。
      class airPlane{
          virtual void fly(){
              ... // 缺省的实现
          }
          ...
      };
      class planeTypeA:public airPlane{...}; // A依赖缺省的fly()方法
      class planeTypeB:public airPlane{...}; // B也一样
      class planeTypeC:public airPlane{...}; // C的引擎与A和B不一样,但是忘了实现自己的fly()方法!

       一个可选的方法是,定义一个非虚函数,令虚函数调用它

      class airPlane{
      public:
          virtual void fly() =0;
      protected:
          void defaultFly(){
              // 缺省的实现
          }
      };
      class planeTypeA:public airPlane{
          void fly(){
              defaultFly(); // 即使依赖缺省实现,也要显式调用
          }
      };
      class planeTypeC:public airPlane{
          void fly(){
              // C的引擎与A不一样,不能依赖缺省实现,这里是单独的一份实现
          }
      };
    • 基类中的非虚函数,表示派生类不仅需要继承接口,还需要继承一份强制的实现。

    35. 考虑虚函数以外的选择

    • 非常精彩的一节!这一节在 为对象实现“动态的方法” 这个话题上,提供了四种不同的风格:
      • 接口不含虚函数的Template Method模式
        这种模式认为,虚函数都必须是private的,基类的“动态逻辑”(即不同派生类不同的逻辑)由非虚函数调用虚函数实现。假设我们在设计网络游戏《魔兽世界》每个种族的跳跃动作:
        class charactor
        {
        public:
            void jump(){
                ... // 准备工作,比如停止施法(如果正在)
                doJump(); // 跳跃
            }
        private:
            virtual void doJump() =0; // 跳跃
        };

        侏儒的跳跃与人类的肯定不一样,所以派生类需要实现基类中的纯虚函数。

        class dwarfCharactor:public charactor{
        private:
            void doJump(){
                // 侏儒角色的跳跃动作
            }
        };
        class orcCharactor:public charactor{
        private:
            void doJump(){
                // 兽人角色的跳跃动作
            }
        };

        这种模式的有点在于,你可以做一些“事前”或“事后”的事情,比如跳跃时必须停止施法。但是这种模式会产生这样的诡异之处:派生类需要实现一个根本不需要自己调用的函数(而是给基类的函数调用),也就是说基类保留了“何时调用该函数的权利”,却将函数的细节交给派生类掌管。

      • 函数指针实现的Strategy模式
        兽人不一定是指玩家,也可能是指怪物。如果游戏中有大大小小各色兽人怪物,他们的跳跃方式只有在初始化时才能确定,那么我们可以在类中保存一个函数指针,在初始化时传入函数地址。
        void defaultJump();
        
        class charactor{
        public:
            charactor(void (*jump)()=defaultJump):
              jumpFunc(jump)
              {}
        private:
            void (*jumpFunc)();
        };
        
        class orcCharactor:public charactor{
        public:
            orcCharactor(void (*jump)()=defaultJump):
              charactor(jump)
              {}
        };

        通过建立如 setJumpFunc 函数甚至可以在运行时改变角色跳跃的方式。

      • tr1::function实现的Strategy模式
        将函数指针实现的Strategy模式中的“函数指针”替换为函数对象tr1::function。假设我们现在要计算角色剩余的生命值(好吧,还是用书中的例子吧,编不下去了,但是这里真的很精彩啊!为了避免以后忘记,一定要好好记下来,嗯)。
        class charactor{
        public:
            // std::tr1::function<int (const charactor*)>对象healthCalc,可以接受一个类似函数的对象,只要该对象能够:
            // 返回一个与int兼容的对象/变量
            // 接受一个与const charactor&兼容的对象/变量
            charactor(std::tr1::function<int (const charactor*)> _healthCalc):healthCalc(_healthCalc){}
        private:
            std::tr1::function<int (const charactor&)> healthCalc;
        };
        class orcCharactor:public charactor{...};

        类 charactor 中包含一个 std::tr1::function<int (const charactor*)> 类型的成员对象 healthCalc ,该对象可以通过任何“像函数的东西”来初始化。如注释中所说,只要这个东西接受和返回具有相应兼容性的对象,就可以初始化healthCalc。比如以下这三样东西:

        short calcHeath(const charactor&); // 计算生命值的函数
        struct healthCaculator{ // 函数对象
            int operator()(const charactor&) const;
        };
        class gameLevel{
        public:
            float healh(const charactor&); // 某个类的成员函数
        };
        一个函数,一个函数对象,一个类的成员函数。他们都可以用来初始化。
        orcCharactor badGuy1(calcHeath); // 用函数初始化
        orcCharactor badGuy2(healthCaculator); // 用函数对象初始化
        gameLevel level;
        orcCharactor badGuy3( // 使用成员函数初始化
            std::tr1::bind(&gameLevel::healh, level, _l)
            );

        我们分别使用函数和函数对象来进行初始化。最精彩的在第三个,使用成员函数初始化。因为成员函数实际上额外接受一个参数(即调用成员函数的对象自身),所以它实际上是接受两个参数的函数。而std::tr1::bind方法允许为这样一个函数的其中一个参数绑上默认值,使这个函数的行为就像是只接受一个参数的函数那样。这个方法同样适用于具有多个参数的函数(而不仅仅是成员函数,这里拿成员函数只不过又提醒了我,成员函数隐式接受调用对象自身作为参数)。这真的很奇妙。

      • 古典Strategy模式
        相对简单,将计算生命值 和 角色 分别体系化,角色基类 中 存储 指向“计算生命值基类对象”的指针,并在派生类中实现相应逻辑。通常使用UML图描述这种关系。

    36. 绝不重新定义继承而来的非虚函数

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

    • 非虚函数和缺省参数值都是静态绑定的,对于虚函数中的缺省参数值,是否会影响到派生类中的对应函数,取决于调用的形式。比如:
      class B{
          virtual void f(int x=8){}
      };
      class D:public B{
          void f(int x){}
      };

       这种情况下,如果通过指向派生类实例的基类指针调用函数f(),可以不指定参数x,缺省参数值起作用。但是如果通过派生类指针调用函数f(),不指定参数x就无法通过编译。

    • 注意B中的函数f()是虚函数。不应当在public继承的派生类中重载基类的非虚函数。

    38. 通过复合模拟出 has-a 或者 is-implemented-in-terms-of 关系

    • 应用域:has-a关系。
    • 实现域:is-implemented-in-terms-of 关系。

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

    • private 继承的特点是:基类中的所有public成员都将称为派生类的private成员,从派生类外无法访问基类的成员。这说明基类的逻辑被隐藏在幕后,派生类需要借助基类实现其自身的功能,即 is-implemented-in-terms-of 关系。
    • 与复合不同之处:private继承的派生类具有“对象尺寸最小化”的特征。如下,类B1和类B2都是通过B来实现的(在这里B只是个什么都没有的空类)。但是在几乎所有编译器中,B2对空间的消耗的确比B1稍大一些。
      class B{};
      class B1:private B{};
      class B2{
      private:
          B b;
      };

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

    • 多重继承,顾名思义,就是同时继承多个基类。在访问多重继承派生类的时候,如果多个基类中的成员具有相同的名称,需要显示指定访问的是哪个基类中的成员,如:
      class B1{
      public:
          void f(){};
      };
      class B2{
      public:
          void f(){};
      };
      class D:public B1, public B2{};
      此时需要:
      D d;
      d.B1::f();
    • 解决钻石型多重继承:如果多重继承的两类又同时继承自同一类,如:
      class B{
      public:
          int x;
      };
      class B1:public B{};
      class B2:public B{};
      class D:public B1, public B2{}; 
      此时类D中实际上有两份x(B1::x和B2::x),这两份x又同时继承自B。在语义上往往只要一份x。C++默认的实现是,维持两份x,但是相互复制。改动一份则两份都受到影响。一种语义上更自然,但是却会造成额外开销的方法是,将B1和B2对B的继承都实现为 virtual public 继承,这样在类D中就只有一份x了。
    • 使用 virtual 继承会产生额外的开销,而且virtual继承后,基类的初始化由最底层的派生类实现(也就是说,D要负责对B中成员x的初始化,而不是由B1和B2负责)。所以,如果不得不使用virtual继承,那么就尽量避免在可能被virtual继承的基类中放置数据。

    41.隐式接口和编译器多态

    • 隐式接口是泛型编程中的概念,相对的显式接口则是面向对象编程中的。
    • 显式接口,包括函数的签名,或者类的public部分,它规定了类和函数能够做什么,外界如果才能驱动函数和类的工作。
    • 隐式接口,指在一个模板元中,待给定的类T需要做什么。比如:
      template <typename T>
      void compareSize(T& t1, T& t2){
          return t1.size()>t2.size();
      }
    • 在这个模板元中,T的隐式接口就是,必须具有size()方法,而且该方法返回的对象重载了>运算符,或者是内置类型。在模板的“具现化”过程中,不会发生什么,但是如果编译到调用compareSize<int>()方法的语句,就会编译出错(因为int没有实现size()方法)。

    42.了解typename的双重含义

    • 从属属性:在模板中依赖于一个template参数(也就是尖括号中typename后面的T啦)的属性(注意,是属性而不是成员哦)。
    • 在使用从属属性的时候,应当在前面加上一个typename关键字,否则就会引发潜在的问题,如下所示。如果在T::someProperty前没有typename关键字,也许编译器会把声明指针用的*认为是用作乘法的乘号。
      template <typename T>
      class C{
      public:
          void f(){
              typename T::someProperty* x;
          };
      };

    43.处理模板化基类内的名称

    • 当基类是一个模板类时,派生类对基类几乎一无所知。事实就是这样,下面这段代码,在严格的编译器中,无法通过编译。虽然基类中已经定义了f()函数,但是派生类却坚持看不到这个函数。(但是我在VS2012中却是可以编译的,而且就算我把f()改成f2()都是可以编译的(f2()在基类B中可没有定义),只要不去实例化某个D类的对象,也就是说编译器对基类的假定相当宽松,把很多事情交给了编译后期完成)。
      template <typename T>
      class B{
      public:
          void f(){}
      };
      template <typename T>
      class D:public B<T>{
      public:
          void callf(){
              f(); // 无法通过编译!
          }
      };

      这是因为编译器知道,B类可能被特化,因此严格的编译器拒绝让D中对f()的调用通过编译。

      template <>
      class B<int>{
      public:
          // B模板类的这个特化版本并没有f()方法
      };

      解决这个问题的方法有三种:11

    • 使用this指针
      template <typename T>
      class D:public B<T>{
      public:
          void callf(){
              this->f();
          }
      };
    • 使用using语句
      template <typename T>
      class D:public B<T>{
      public:
          using B<T>::f;
          void callf(){
              f();
          }
      };
    • 明确指定调用的函数存在于基类中
      template <typename T>
      class D:public B<T>{
      public:
          void callf(){
              B<T>::f();
          }
      };

    44.将与参数无关的代码抽离templates

    •  非类型模板参数往往引起“代码膨胀”。如
      template <typename T, int size>
      class mat{
      public:
          mat invert(); 
      ...
      };

       就不如:

      template <typename T>
      class mat{
      public:
          mat invert();
      ...
      private:
          int size;
      };
    作者:一叶斋主人
    出处:www.cnblogs.com/yiyezhai
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    egret 示例实战六:利用Timer定时器,实现钟表秒针行走效果
    egret 示例实战六:延迟操作,实现打字效果
    egret:什么是脏矩形
    egret 示例实战五:随机画圆
    egret 示例实战四:圆弧遮罩
    egret 示例实战三:点击不同对象提升至最上层
    egret 示例实战二:实现爱心缩放和旋转动画
    egret 示例实战一:轻触屏幕调整显示对象位置
    egret:tabBar怎么取消默认选中呢?
    egret:ViewStack 中的scroller滚动条的隐藏
  • 原文地址:https://www.cnblogs.com/yiyezhai/p/2961999.html
Copyright © 2020-2023  润新知