【讀書筆記】Effective C++(06)繼承與面向對象

作者: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繼承。
發佈了52 篇原創文章 · 獲贊 19 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章