Effective C++ 读书笔记(六)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40028201/article/details/89673286

Effective C++ 读书笔记(六)

6 、继承和面向对象设计

条款 32.确定你的public继承朔模出is-a关系

  1. 以C++面向对象编程,最重要一个规则是:public inheritance(公开继承)意味着“is-a”(是一种)的关系。在这里是“直译”,例如class D: public B直译就是D是一种B。
  2. “public”继承意味着is-a。适用于base classes身上的每一件事一定也适用于derived身上,因为每一个derived对象也是一个base class 对象。

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

  1. 对于变量和函数,子类的名称会遮掩父类的名称,即使是函数重载也不行

     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();//正确,调用Derived::mf1
        d.mf1(x);//错误,因为Derived::mf1遮掩了Base::mf1
        d.mf2();//正确,调用Base::mf2
        d.mf3();//正确,调用Derived::mf3
        d.mf3(x);//错误,因为Derived::mf3遮掩了Base::mf3
    

这里写图片描述

​ 因为以作用域为基础的“名称遮掩规则”,base class内所有名称为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3都不再被Derived继承。

  1. 为了避免这个问题,可在子类中使用 using 声明或使用 转交函数(forwarding functions)。如:

    1. 使用using声明;会把所有的都继承过来

       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:
              //让Base class内名为mf1和mf3的所有东西在Derived作用域内都可见,且为public
              using Base::mf1;
              using Base::mf3;
              virtual void mf1();
              void mf3();
              void mf4();
              ……
          };
      

      则上面的就不会出错了

       Derived d;
          int x;
          d.mf1();//正确,调用Derived::mf1
          d.mf1(x);//调用Base::mf1
          d.mf2();//正确,调用Base::mf2
          d.mf3();//正确,调用Derived::mf3
          d.mf3(x);//调用Base::mf3
      
    2. 交换函数

      仅仅只想继承Base内mf1无参数的那个版本,则需要用一个简单的交换函数

          class Base{
          public:
              virtual void mf1()=0;
              virtual void mf1(int);
          };
          class Derived: private Base{
          public:
              virtual void mf1()//转交函数(forwarding function)
              {Base::mf1();};//隐式成为inline
          };
          Derived d;
          int x;
          d.mf1();//调用Derived::mf1
          d.mf1(x);//错误,Base::mf1被遮掩了
      

条款 34 :区分接口继承和实现继承

  1. 纯虚函数目的是让子类之继承函数接口,不关心子类怎么去实现
  2. 非纯虚函数是为了让子类可以选择的实现这个接口,或是父类的默认实现
  3. 成员函数目的是让子类继承这个函数以及他的实现,而不去改变他(所有子类都使用这个相同的实现)

条款35 : 考虑 virtual 函数以外的其它选择(学习了设计模式之后再来看)

  1. 使用 non-virtual interface(NVI)手法,这是一种名为 模版方法 模式的设计模式,使用成员函数包裹虚函数。

     class GameCharacter{
        public:
            int healthValue() const
            {
                ……  //做事前工作
                int retVal=doHealthValue();//真正做实际工作
                ……  //做事后工作
                return retVal;
            }
            ……
        private:
            virtual int doHealthValue() const //derived classes可以重新定义
            {
                ……
            }
        };
    
  2. 将虚函数替换为函数指针的成员变量。Strategy设计模式的应用

        class GameCharacter;//forward declaration
        int defaultHealthCalc(const GameCharacter& gc);//健康计算缺省算法
        class GameChaaracter{
        public:
            typedef int(*HealthCalcFunc)(const GameCharacter&);
            explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
                :healthFunc(hcf)
            {}
            int healthValue()const
            { return healthFunc(*this); }
            ……
        private:
            HealthCalcFunc healthFunc;//函数指针
        };
    
    • 同一人物类型之不同实体可以有不同的健康计算函数。也就是说同一人物类型不同的对象可以有不同的健康计算,例如射击游戏中,一些购买防弹衣的玩家使用的对象,血量可以减少更慢。
    • 某已知人物健康计算函数可以在运行期间变更。即健康计算函数不再是GameCharacter继承体系内的成员函数。
  3. 将虚函数替换为 tr1::function

    扫描二维码关注公众号,回复: 6057779 查看本文章
  4. 将继承体系内的虚函数替换为另一个继承体系内的虚函数(策略模式)。

条款 36:绝不重新定义继承而来的non-virtual 函数

  1. 因为non-virtual函数是静态绑定的(statically bound,条款 37)。pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本。但是virtual函数是动态绑定(dynamically bound,条款 37),所以virtual函数不受这个约束,即通过指针调用,实际调用的函数是指针真正指向对象的那个函数。

    class B{
    public:
        void mf();
        ……
    };
    
    class D: public B {……};
    
    //调用
    D x;
    B* pB=&x;
    pB->mf();//虽然指向的是派生类对象,但是只能是B所定义的版本,不是多态的行为
    D* pD=&x;
    pD->mf();
    
    
  2. 记住 任何情况下都不该重新定义一个继承而来的 non-virtual 函数。

    条款 7已经知道,base class内的析构函数应该是virtual;如果你违反了条款 7,你也就违反了本条款,析构函数每个class都有,即使自己没有编写

条款 37 :绝不要重新定义继承而来的缺省参数

  1. 对于以下继承

    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;//不同缺省参数值,很糟糕
        ……
    };
    class Circle: public Shape{
    public:
        virtual void draw(ShapeColor color) const;
        /*客户调用上面函数时,如果使用对象调用,必须指定参数值,因为静态绑定下这个函数不从base继承缺省值。*/
        /*如果使用指针或引用调用,可以不指定缺省参数值,动态绑定会从base继承缺省参数值*/
        ……
    };
    
    
  2. 对象的静态类型是他在程序中被声明时产生的类型,动态类型可以表现出一个对象将会有什么行为,动态类型可以在执行过程中改变,重新赋值可以改变动态类型。

    Shape* ps;
    Shape* pc=new Circle;//pc动态类型是Circle*
    Shape* pr=new Rectangle;//pr的动态类型是Rectangle
    

    这些指针都是静态类型Shape*,动态类型是指目前所指的对象类型

  3. virtual函数是动态绑定的,调用哪一份函数实现的代码,取决于调用的那个对象的动态类型。

    pc->draw(Shape::Red);
    pr->draw(Shape::Red);
    
  4. 如果不带有参数类型

    pr->draw();//调用Rectangle::draw(Shape::Red)
    

    上面调用中,pr动态类型是Rectangle*,所以调用Rectangle的virtual函数。Rectangle::draw函数缺省值是GREEN,但是pr是静态类型Shape*,所以这个调用的缺省参数值来自Shape class,不是Rectangle class。这次调用两个函数各出了一半的力。

  5. 想要 virtual 函数表现出你想要的行为却有遇到麻烦,聪明的做法就是使用条款 35 里的替代方案。

条款38 :通过复合塑模出has-a或“根据某物实现出”

  1. 复合(也叫组合 composition)是 has-a 关系,指 “有一个”。我们将某种类型中包含其它类型的关系叫做复合。

    class Address{ …… };
    class PhoneNumber{ …… };
    class Person{
    public:
        ……
    private:
        std::string name;
        Address address;
        PhoneNumber mobilePhone;
    };
    
    

    Person对象中包含string,Address,PhoneNumber对象,这就是复合。还有几个同义词:layering(分层),containment(内含),aggregation(聚合),embedding(内嵌)

  2. 如果两个类之间并非严格的 is-a 关系(set与list),继承不再适用,我们应该选择复合的形式实现它们。

条款39 :明智而审慎的使用private继承

  1. private 继承意味着父类所有非私有成员在子类中都是 private 的。这样就帮我们复用父类代码且防止父类接口曝光(只能子类内部使用)。
  2. 但是私有继承意味着不再是 is-a 关系,而更像是 has-a 关系。我们总是可以通过复合的方式替代私有继承,并且更容易理解,所以无论什么时候,只要可以,我们还是应该选择复合
  3. 一种极端情况下,即我们有一个空白的父类(不包含non-static变量、virtual函数,没有继承virtual base class,但是含有typedef、enum、static或弄-virtual函数),私有继承可以更小的占用空间。

条款40:明智而审慎的使用多重继承

  1. 多继承比单继承复杂,而且多继承可能导致二义性(必须明确指出调用类名::),以及对 virtual 继承的需要。

  2. 多重继承还会导致菱形继承

     	class File{……};
        class InputFile: public File{……};
        class OutputFile: public File{……};
        class IOFile:public InputFile, public OutputFile
        {……};
    

    IOFile里面有File的两重拷贝(如果File里面有个成员变量,则IOFile里面有两份,是重复的)

    解决方案:

    1. 缺省方案(就上面所说的执行复制)

    2. 让带有此数据的class即(File)成为一个虚基类

      虽然避免了成员变量重复,但是编译器付出了代价,使用virtual继承的对象比non-virtual继承的对象体积大,访问virtual base classes成员变量是,也比访问non-virtual base classes成员变量速度慢。

          class File{……};
          class InputFile: virtual public File{……};
          class OutputFile:virtual public File{……};
          class IOFile:public InputFile, public OutputFile
          {……};
      
  3. 父类中存在数据的话,virtual 继承会增加大小、速度、初始化(及赋值)复杂度等成本,应尽量不使用 virtual 继承。

    对虚拟继承的忠告:

    1. 非必要时,不使用virtual继承,平时使用non-virtual继承。
    2. 如果必须使用virtual继承,那么尽量避免在base内放置数据,这样就不用担心在这些classes身上的初始化和赋值带来的诡异事情了
  4. 多继承适用的一种场景是:public 继承某个接口类并 private 继承某个协助实现的类

        class IPerson{
        public:
            virtual ~IPerson();
            virtual std::string name() const=0;
            virtual std::string birthDate() const=0;
        };
        class DatabaseID{……};
        class PersonInfo{
        public:
            explicit PersonInfo(DatabaseID pid);
            virtual ~PersonInfo();
            virtual const char* theName() const;
            virtual const char* theBirthdayDate() const;
            ……
        private:
            virtual const char* valueDelimOpen() const;
            virtual const char* valueDelimClose() const;
            ……
        };
        class CPerson: public IPerson, private PersonInfo{
        public:
            explicit CPerson(DatabaseID pid): PersonInfo(pid){}
            virtual std::string name() const
            {
                return PersonInfo::theName();
            }
            virtual std::string birthDate()
            {
                return PersonInfo::theBirthDate();
            }
        private:
            const char* valueDelimOpen() const{return "";}
            const char* valueDelimClose() const{return "";}
        };
    

猜你喜欢

转载自blog.csdn.net/qq_40028201/article/details/89673286