《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;
    };
posted @ 2013-04-07 20:44 一叶斋主人 阅读(...) 评论(...) 编辑 收藏