乐趣区

读书笔记Effective-C06继承与面向对象

6. 继承与面向对象

  • 6.1 条款 32:确定你的 public 继承是 is- a 关系

    • public 继承的子类对象需要保证可以被视作父类对象来调用函数。
    • class Person {...};
      class Student : public Person {...};
      
      void eat(const Person& p);
      void study(const Student& s);
      eat(p);   // ok
      eat(s);   // ok,Student 可以视作 Person 调用函数
      study(s);   //ok
      study(p);   //error,Person 不能视作 Student
  • 6.2 条款 33:避免遮掩继承而来的名称

    • int x;  //global 变量
      void someFunc() {
          double x;  //local 变量
          std::cin >> x;  //local 变量的 x 遮掩了 global 变量的 x,实际起作用的是 local 变量的 x
      }
    • // 定义基类
      class Base {
      private:
          int x;
      public:
          virtual void mf1() = 0;
          virtual void mf1(int);
          virtual void mf2();
          void mf3();
          void mf3(double);
          ...
      };
      // 定义派生类
      class Derived : public Base {
      public:
          virtual void mf1();
          void mf3();
          void mf4();
          ...
      };
      
      // 使用
      Derived d;
      int x;
      ...
      d.mf1();    //ok,调用 Derived::mf1
      d.mf1(x);   //bad,因为 Derived::mf1 遮掩了 Base::mf1
      d.mf2();    //ok,调用 Base::mf2
      d.mf3();    //ok,调用 Derived::mf3
      d.mf3(x);   //bad,因为 Derived::mf3 遮掩了 Base::mf3
    • // 解决方法 1
      // 定义派生类
      class Derived : public Base {
      public:
          using Base::mf1;    // 让基类中名为 mf1 和 mf3 的所有东西在此作用域内可见
          using Base::mf3;
          virtual void mf1();
          void mf3();
          void mf4();
          ...
      };
      
      // 使用
      Derived d;
      int x;
      ...
      d.mf1();    //ok,调用 Derived::mf1
      d.mf1(x);   //ok,调用 Base::mf1
      d.mf2();    //ok,调用 Base::mf2
      d.mf3();    //ok,调用 Derived::mf3
      d.mf3(x);   //ok,调用 Base::mf3
    • // 解决方法 2
      // 定义基类
      class Base {
      public:
          virtual void mf1() = 0;
          virtual void mf1(int);
          ...
      };
      // 定义派生类
      class Derived : public Base {
      public:
          virtual void mf1() { Base::mf1(); }  // 转交函数
          ...
      };
      
      // 使用
      Derived d;
      int x;
      ...
      d.mf1();    //ok,调用 Derived::mf1
      d.mf1(x);   //bad,因为 Base::mf1 被遮掩,且 Base::mf1(int) 没有被转交 
  • 6.3 条款 34:区分接口继承和实现继承

    • 如下代码所示,有 3 类继承关系:

      • 纯虚函数的继承,目的是让派生类只继承函数接口。派生类必须实现该接口。
      • 非纯虚函数的继承,目的是让派生类继承函数接口和缺省实现。派生类可以实现该接口,也可以选择使用基类的缺省实现。
      • 非虚函数的继承,目的是让派生类继承函数接口和强制实现。派生类不应该另外实现该接口,否则发生上一个条款所述的遮掩行为。
    • class Shape {
      public:
          virtual void draw() const = 0;
          virtual void error(const std::string& msg);
          int objectID() const;
          ...
      };
      
      class Rectangle : public Shape {...};
      class Ellipse : public Shape {...};
  • 6.4 条款 35:考虑 virtual 函数之外的其他选择

    • // 假设你正在写一个游戏软件,每个游戏人物都应该有 "健康" 这个属性
      class GameCharacter {
      public:
          virtual int healthValue() const;  // 返回游戏人物的健康状态
          ...
      };
    • 上面的写法没有问题,基类提供了接口和一个缺省的实现,派生类可以选择是否重写这个函数。但是,作者也提醒,还有一些其它的写法:

      • // 方法 1:借由 Non-Virtual Interface(NVI)手法实现 Template Method 模式
        class GameCharacter {
        public:
            int healthValue() const {
                ... // 在开始主函数前,可以做一些额外的工作
                int retVal = doHealthValue();   // 这个函数可以被子函数重写
                ... // 在结束主函数后,可以做一些额外的工作
                return retVal;
            }
            ...
        private:
            virtual int doHealthValue() const {...}
        };
      • // 方法 2:借由函数指针实现 Strategy 模式
        // 这种方法允许同一个派生类的不同对象使用不同的函数计算健康状态
        class GameCharacter {
        public:
            typedef int (*HealthCalcFunc)(const GameCharacter&);
            explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
              : healthFunc(hcf) { }
            int healthValue const {return healthFunc(*this);
            }
            ...
        private:
            HealthCalcFunc healthFunc;
        };
      • // 方法 3:借由 tr1::function 完成 Strategy 模式
        // 这种方法比函数指针更自由,它可以是函数指针,也可以是任何可以被调用的东西
        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;
        }
      • // 方法 4:古典的 Strategy 模式
        //HealthCalcFunc 来自另一个继承体系
        class GameCharacter;
        class HealthCalcFunc {
        public:
            ...
            virtual int calc(const GameCharacter& gc) const {...}
            ...
        };
        HealthCalcFunc defaultHealthCalc;
        
        class GameCharacter {
        public:
            explicit GameCharacter(HealthCalcFun* phcf = &defaultHealthCalc)
                : pHealthCalc(phcf) { }
            int healthValue() const {return pHealthCalc->calc(*this);
            }
            ...
        private:
            HealthCalcFunc* pHealthCalc;
        };
  • 6.5 条款 36:绝不重新定义继承而来的 non-virtual 函数

    • 我怀疑作者这条讲重复了,条款 33 讲 ” 遮掩 ” 的时候就讲到了这个内容
    • // 基类
      class B {
      public:
          void mf();
          ...
      };
      // 派生类
      class D {
      public:
          void mf();   // 遮掩 B::mf
          ...
      };
      // 使用
      D x;
      B* pB = &d;   // 调用 B::mf()
      pB->mf();
      D* pD = &d;   // 调用 D::mf()
      pD->mf();
      // 如果 B::mf() 是 virtual 函数,则上面两处都是调用 D::mf()
      // 所以也就可以解释,条款 7 所述的 "使用多态时,基类的析构必须为 virtual"
  • 6.6 条款 37:绝不重新定义继承而来的缺省参数值

    • 条款 36 说了 non-virtual 不要重新定义,所以在派生类中能重新定义的是 virtual 函数。
    • virtual 函数是动态绑定,而缺省参数值是静态绑定,这就会带来问题。
    • // 基类
      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;
          ...
      };
      // 使用
      Shape* pR = new Rectangle;  // 请注意类型是 Shape*
      pR->draw();   // 调用 Rectangle::draw(Shape::Red),因为缺省值是在编译期间静态绑定的,而 pR 的静态类型为 Shape*,是基类 
    • 解决方法是在条款 35 中找一种替代设计,比如 NVI。
    • // 基类
      class Shape {
      public:
          enum ShapeColor {Red, Green, Blue};
          void draw(ShapeColor color=Red) const {doDraw(color);
          }
          ...
      private:
          virtual void doDraw(ShapeColor color) const = 0;
      };
      // 派生类
      class Rectangle : public Shape {
      public:
          ...
      private:
          virtual void doDraw(ShapeColor color) const {...}
          ...
      };
  • 6.7 条款 38:确保 ” 复合 ” 是 has- a 或 is-implemented-in-terms-of 关系

    • 复合(composition)是类之间的一种关系,不同于 public 继承,它代表 has- a 或 ” 根据某物实现出 ” 这样一种关系。
    • class Address {...};
      class PhoneNumber {...};
      class Person {
      public:
          ...
      private:
          std::string name;
          Address address;
          PhoneNumber voiceNumber;
          PhoneNumber faxNumber;
      };
  • 6.8 条款 39:明智而审慎地使用 private 继承

    • private 继承会将基类中继承而来的所有成员变为 private 成员。
    • 如下代码所示,private 继承的关系更像是 ” 复合 ”(Student 类中带有 Person 类的一些功能),只有软件实现层面的意义,不具有设计层面上的意义。尽可能使用 ” 复合 ” 替代 private 继承。
    • class Person {...};
      class Student : private Person {...};
      void eat(const Person& p);
      
      Person p;
      Student s;
      eat(p);   //ok
      eat(s);   //bad,Student 不能被视为 Person
  • 6.9 条款 40:明智而审慎地使用多重继承

    • 多重继承会发生 ” 成员函数或成员变量重名 ”、” 钻石型继承 ” 等问题,对于这种情况建议使用 virtual public 继承,但这会带来空间和效率的损失。
    • 多重继承的复杂性,导致一般不会使用。只有 virtual base classes(也就是继承多个接口)才最有实用价值,它不一定需要 virtual public 继承。
退出移动版