• Effective C++ 6 继承与面向对象设计 学习记录


    条款32确定public继承is-a的意义符合你的设计意图

    Class Person;

    Class Student : public Person{};

    Void study(const  Student &s);

    Void eat(const Person &p);

    Person p;

    Student s;

    eat(p);

    eat(s);

    Study(p);

    Study(s);

    Public继承代表的是is-a的关系。对于eat()函数,编译器可以将Student对象转型为Person对象,而对于study()函数而言,Person对象不一定是Student对象,因此反过来就行不通。如果想要程序编译通过,可能要进行强制类型转换。

    合理的设计思维

    Public继承意味着父类对象上可以实施的任何操作,子类对象必须也可以实施。因为每个子类对象都包括一个父类对象。

    条款33:避免继承而来的名称

    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();

    d.mf1(x);

    d.mf2();

    d.mf3();

    d.mf3(x);

    对于mf3()函数是子类non-virtual函数覆盖了父类non-virtual函数,条款36有详细的解释。这里我们主要讨论子类定义覆盖父类定义的现象。由于覆盖现象导致父类一些定义不再可见,从而导致错误。这与public继承is-a的意义相左。子类public继承与父类,却无法使用父类public成员函数。

    合理的设计思维

    public继承会覆盖父类内的名称,有时这会违反public的意义。可以采用using显示申明需要可见的父类成员函数。

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

    Class Shape

    {

     Public:

     Virtual void draw()cosnt=0;

     Virtual void error(const string &msg);

     Int objectID() const

    };

    Class Rectangle: public Shape{};

    Class Ellicpse: public Shape{};

    首先需要明确的是接口继承与实现继承是不同的概念,pure virtual函数是可以有实现的。上述类定义中,对于pure virtual函数,当子类public继承的时候,要求一定要有自己的实现。因此,声明一个pure virtual函数的目的是为了继承接口;对于声明impure virtual函数的目的是为了继承接口和缺省实现;对于声明non-virtual函数的目的是为了继承接口和强制实现,不容更改定义。

    合理的设计思维

    Public继承下,子类总是继承父类的接口。因此,是否需要继承实现、重新实现还是不容改变就决定了函数声明的方式,是pure virtualimpure virtual还是non-virtual

    条款35考虑virtual函数以外的其他选择

    1. 如条款37所述的NVI设计方法

    案列见条款37,我们需要明白一点,NVI设计方法并不一定需要virtual函数为private。如果需要在外覆器wrapper中调用基类的对应兄弟,则可以将其声明为protectedVirtual析构函数的声明一定要是public,详细见条款7NVItemplate method设计模式的一个独特表现形式。

    2. non-member函数替代

    Class GameCharacter;

    Int defaultHealthCalc(const GameCharacter &gc);

    Class GameCaracter

    {

     Public

     Typedef int (*HealthCalcFunc)(const GameCaracter &);

     Explicit GameCaracter(HealthCalcFunc hcf=defaultHealthCalc): healthFunc(hcf){}

     Int healthValue()const

     {

      Return healthFunc(*this);

     }

     Private:

     HealthCalcFunc healthFunc;

    };

    Class EvilBadGuy: public GameCharater

    {

     Public:

     Explicit EvilBadGuy(HealthCalcFunc hcf=defualtHealthCalc): GameCharacter(hcf){}

    };

    Int loseHealthQuickly(const GameCharacter &);

    Int loseHealthSlowly(const GameCharacter &);

    EvilBadGuy ebg1(loseHealthQuickly);

    EvilBadGuy ebg2(loseHealthSlowly);

    此时,类中定义了一个指向函数的指针变量。对于同一类实体,现在可以调用不同健康计算函数,因而更加灵活。同时,如果在类中再定义一个可以设置指针变量的函数SetHealthCalcFunc,那么对象的健康计算方式可以在运行期进行改变。但这种设计方式也有缺点,比如健康计算函数需要使用到类中privateprotected成员变量和成员函数,而此时并没有提供这些接口,那么就存在实现上的不便。此时,可以弱化类的封装,将这个健康计算函数声明为其friend函数,或者为能够使用类中成员变量或函数提供public接口。此种设计方法是strategy设计模式的简单应用。

    3. 借助tr1::function完成strategy模式

    可以借助templates,来实现上述借由函数指针实现的strategy模式。

    Class GameCharacter;

    Int defaultHealthCalc(const GameCharacter &gc);

    Class GameCaracter

    {

     Public

     Typedef  std::tr1::function<int (const GameCharacter &)> HealthCalcFunc;

     Explicit GameCaracter(HealthCalcFunc hcf=defaultHealthCalc): healthFunc(hcf){}

     Int healthValue()const

     {

      Return healthFunc(*this);

     }

     Private:

     HealthCalcFunc healthFunc;

    };

    4. 经典strategy模式

    Class GameCaracter;

    Class HealthCalcFunc

    {

     Public:

     Virtual int calc(const GameCharacter &gc) const

     {

     } 

    };

    HealthCalcFunc defaultHealthCalc;

    Class GameCaracter

    {

     Public

     Explicit GameCaracter(HealthCalcFunc *hcf=&defaultHealthCalc): healthFunc(hcf){}

     Int healthValue()const

     {

      Return healthFunc->calc(*this);

     }

     Private:

     HealthCalcFunc *healthFunc;

    };

    此时,可以通过为HealthGalcFunc类添加子类,并在覆盖calc()函数,就可以实现不同的健康计算方式。

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

    Class B

    {

     Public

     Void mf();

    };

    Class D: public B

    {

     Public:

     Void mf();

    };

    D继承B,且D覆盖了Bnon-virtual成员函数。

    D x;

    B *pB=&x

    D *pD=&x;

    pB->mf();

    pD->mf();

    对于non-virtual成员函数(此时静态类型和动态类型之分),编译器实施静态绑定。因此,对象D表现怎么得行为取决于指向该对象的指针类型,而非对象本身。

    合理的设计思维

    1.public继承(public继承是is-a的关系);

    2.定义virtual成员函数。

    因此,此条款对于条款7也是一个合理的解释。

     

    条款37:绝不重新定义继承(virtual函数)而来的缺省参数值

    Class Shape

    {

     Public:

     Enum ShapColor{Red  Green  Blue};

     Virtual void draw(ShapeColor color=Red) const=0;

    };

    Class Rectangle:Public Shape

    {

     Public:

     Virtual void draw(ShapeColor color=Green) const=0;

    };

    Class Circle:Public Shape

    {

     Public:

     Virtual void draw(ShapeColor color) const=0;

    };

    virtual 函数不同,virtual 函数缺省参数值采用静态绑定,因此子类对象调用动态绑定的virtual函数时,缺省参数值却由父类指定。 

    Shape *ps=new Rectangle;

    Ps->draw();

    Rectangle中虽然修改了缺省的参数值,但draw()的缺省参数值还是Red,并没有符合设计者的预期效果。

    Circle c;

    c.draw(Shape::Red);

    Shape *ps=new Circle ;

    Ps->draw();

    Circlevirtual函数声明与Shape不一致。当对象调用draw()时,静态绑定下的该函数不会从父类继承缺省参数值,因此需要显示提供。而以指针调用draw()时,动态绑定下的该函数会从父类继承缺省参数值。

    合理的设计思维NVIno virtual interface

    Class Shape

    {

     Public:

     Enum ShapColor{Red  Green  Blue};

     Virtual void draw(ShapeColor color=Red) const

      {

        doDraw(color);

      }

    Private:

     Virtual void doDraw(Shape color) const;

    };

    Class Rectangle:public Shape

    {

    Private:

     Virtual void doDraw(Shape color) const;

    };

    此时,draw()是non-virtual成员函数。由条款36可知,其不应该被覆盖,缺省参数设置与成员函数virtual性再无关系。也可以看到父类、子类中doDraw()函数声明一致。

     

    条款38:由聚合实现has-a 或 is-implemented-in-terms-of的类间关系

    1. has-a:

    Class Address;

    Class PhoneNumber;

    Class Person

    {

     Private:

     Std::string name;

     Address address;

     PhoneNumber phonenumber;

    };

    2. is-implemented-in-terms-of

    Template<class T>

    Class Set

    {

     Public:

     Bool member(const T& item) const; //行为

     Void insert(const T& item);

     Void remove(const T& item);

     Std::size_t size() const;

     Private:

     Std::list<T> rep;

    };

    has-a 强调“拥有……”的概念;is-implemented-in-terms-of强调“由……实现”的概念,而又不是is-a的概念,必须重新定义一些行为

    合理的设计思维

    聚合(composition)的意义与public继承(is-a)完全不同。在设计类时,要明确类间关系,从而确定设计方法。

    条款39:理解private继承

    Private继承承载着聚合中is-implemented-in-terms-of的概念。

    Class Person;

    Class Student : private Person{};

    Void eat(const Person &s);

    Person p;

    Student s;

    eat(p);

    eat(s);

    对于public继承,我们知道它是is-a的关系。此例表明private继承不是is-a的关系,因为子类Student对象s作为参数调用eat()时,编译器并没有将s转换成Person对象,而public继承却会这么做。所以private继承只代表实现技术手段而非反应对象间的关系。

    Private继承存在的必要性有三个因素:

    1. protectedpublic成员

    private继承会使父类所有数据成员的访问权限变为private,不可见性避免了使用者一些不恰当的访问操作。

    2.继承并重新定义virtual函数,但又不是is-a的关系

    此时,如果采用public继承,明显不合时宜。通过private继承将virtual函数变为private,避免使用者调用,因为向使用者提供这样的接口并不合适。如有需要可重新定义virtual函数,但必须声明为private,理由很显然。

    3.EBO(空白基类最优化)

    Class Empty{};

    Class HoldsAnInt

    {

     Private:

     Int x;

     Empty e;

    };

    空类以为着这个类中没有virtual函数,没有non-static成员变量,也就意味着sizeofEmpty=0;然而,实际情况是编译器至少会给这样的空类对象分配一个char型大小的空间。因此,对于4四节对齐的系统,sizeofHoldsAnInt=8

    Class HoldsAnInt : private Empty

    {

     Private:

     Int x;

    };

    此时,HoldsAnInt private继承Empty,可以确定sizeofHoldsAnInt =sizeofInt)。看以看到,编译器此时不会再为对象HoldsAnInt 分配一个char型大小的空间。和上面的相比,对象占用空间自然少。然而,空类本身就很特殊,所以不值得提倡。

    合理的设计思维

    尽量使用聚合来表达is-implemented-in-terms-of这种概念。

    条款40:多重继承

    1.多重继承容易造成歧义

    Class BorrowalbleItem

    {

     Public:

     Void chechout();

    };

    Class ElectronicGadget

    {

     Private:

     Bool checkout() const;

    };

    Class MP3Player: public BorrowableItem , public ElectronicGadget

    {

    };

    MP3Player mp;

    Mp.checkout();

    Mp.BorrowableItem ::checkout();

    此时,编译器根据最佳匹配原则寻找最佳函数版本,然而两个函数匹配程度一致,导致歧义。编译器匹配函数主要原则有函数名、参数、返回值和作用域,而此时两个函数只是访问权限的区别,因而需要指明具体的调用函数。

    2.多重继承容易造成代码重复

    Class File{};

    Class InputFile : public File{};

    Class OutputFile :public File{};

    Class InputFile : virtual public File{};

    Class OutputFile :virtual public File{};

    Class IOFile :public InputFile, public OutputFile{};

    此时,IOFile 多重继承InputFileOutputFile,而两者都是继承自File。因此,File的构造函数会被调用两次,IOFile 对象会有两份File成员变量数据。这种情况下,C++引入了虚继承机制。虚继承机制下,多重继承时,子类对象中父类成员变量数据只会有一份。但为了达到这个目的,编译器会做很多工作,对象所占的内容空间也较非虚继承类对象大,变量访问速度也较慢。可以猜想,编译器的大部分工作在于判断和识别继承树中类的virtual性,以及virtual类成员初始化等工作上。

    3. 多重继承可以实现public继承自某接口,private继承自某实现

    Class IPerson

    {

     Virtual ~IPerson();

     Virtual string name() const =0;

     Virtual string birthDate() const=0;

    };

    Class DatabaseID{};

    Class PersonInfo

    {

     Explicit PersonInfo(DatabaseID pid);

     Virtual ~PersonInfo();

     Virtual const char* theName() const;

     Virtual const char* theBirthDate() const;

     Virtual const char* valueDelimOpen() const;

     Virtual const char* valueDelimClose() const;

    };

    Class CPerson : public IPerson, private PersonInfo

    {

     Public:

     explicit CPerson(DatabaseID pid):PersonInfo(pid){}

     Virtual string name() const //继承某接口

    {

     Return PersonInfo::theName();//继承某实现

    }

     Virtual string birthDate() const

    {

     Return PersonInfo::theBirthDate();

    }

     Private:

     Const char* valueDelimOpen() const 

    {

     Return “”;

    }

     Const char* valueDelimClose() const 

    {

     Return “”;

    }

    };

    Public继承是is-a的关系,private继承是聚合的概念。合理的运用不同的继承方式,可以使得代码简洁、合理、易于维护。

    合理的设计思维

    如无必要,尽量使用单继承方法,因为在使用多重继承时,必须考虑歧义和共同继承问题,同时还要考虑多重继承的效率问题。尽量避免虚基类带有任何数据成员。当然,多重继承也有其一定的优越性。上述3反应了在进行类设计时,合理安排多重继承的方式,可以使得类结构简洁且合理。

     

  • 相关阅读:
    nginx 配置7层代理
    还用老套路分析财务数据?这3种财务分析方法,你一定得看看
    nginx 4层代理
    【手绘漫画】面试必考之手撕单链表(解题模板和深度剖析),(LeetCode 707)
    连载三:Oracle升级文档大全
    隐私保护与价值挖掘之利器
    PTA刷题笔记(C语言) | 7-42 整除光棍 (20分)
    django 页面缓存
    C++建议
    读陈浩的《C语言结构体里的成员数组和指针》总结,零长度数组
  • 原文地址:https://www.cnblogs.com/alpha19881007/p/20130807_byalpha.html
Copyright © 2020-2023  润新知