1

作者:LogM

本文原载于 https://segmentfault.com/u/logm/articles,不允许转载~

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继承。

LogM
85 声望18 粉丝