Effective C++之六:继承与面向对象设计

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

(0)

Derived 以 public形式 继承 base,意味着 每一个类型的D的对象同时也(is-a)是一个B的对象,反之不成立。


(1)public继承意味 is-a。适用于base身上的每一件事也一定适用于derived身上,因为每一个derived对象也一定是base对象。

class Person{};

class Student: public Person{};    //以下只对public继承才成立


void eat(const Person& p);

void study(const Student& s);

Person p;

Student s;

eat(p);     //任何人都吃

eat(s);     //学生也是人

study(s);   //学生学习

study(p);   //错误!p不一定是学生。


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


(0)派生类作用域被嵌套再基类作用域内。

class Base{

private:

    int x;

public:

    virtual void mf1() = 0;

    virtual void mf2();

    void mf3();

};


class Derived: public Base{

public:

    virtual void mf1();

    void mf4();

};


void Derived::mf4()

{

    ...

    mf2(); //查找顺序:1、local域(mf4覆盖的作用域)。2、派生类覆盖的作用域。3、基类覆盖的作用域。4、内含base的那个命名空间的作用域。5、global作用域

    ...

}


(1) derived内的名称会遮掩base内的名称。在public继承下从来没有人希望如此。


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);   //派生类遮掩了基类的那个函数,错误!


(2)为了让被遮掩的名称再见天日,可使用using声明式转交函数

上面一段代码中,Derived::mf1对Base的mf1以及mf1(int)造成了名称的遮掩(注意这里继承而来的同名函数就算是参数不同也不能构成重载,都会直接遮掩),如果我们要使用基类的函数。有两种方法:

1、使用using Base::mf1; 对于这样一个表达式,使用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:

    using Base::mf1;    //让基类名为mf1和mf3的所有东西在派生类作用域内都可见

    using Base::mf3();  //这样就构成了重载

    virtual void mf1();

    void mf3();

    void mf4();

};


Derived d;

int x;


d.mf1();    //派生类

d.mf1(x);   //使用基类的那个

d.mf2();    //基类

d.mf3();    //派生类

d.mf3(x);   //使用基类的那个


2.使用转交函数,比如我们派生类的mf1函数可以这样写:virtual void mf1(){Base::mf1()}

在private继承中,我们可能只需要继承一部分函数,这样我们可以使用转交函数的方式。

class Base{

private:

    int x;

public:

    virtual void mf1() = 0;

    virtual void mf1(int);

};


class Derived: private Base{        //派生类唯一想继承的是mf1那个无参数版本。所以就不能用using(会继承所有mf1版本)

public:

    virtual void mf1()  //转交函数

    {

        Base::mf1();    //暗自称为inline

    }

};


Derived d;

int x;


d.mf1();    //派生类

d.mf1(x);   //基类的那个被遮掩了。失败!


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


接口继承和实现继承不同。在public继承之下,derived总是继承base的接口。


(1) 对于纯虚函数:被任何继承了它的具象类重新声明。声明一个纯虚函数是为了让派生类只继承函数接口。告诉继承者:我不管你怎么实现,必须有一个draw函数

class Shape{//有纯虚函数使Shape成为一个抽象类,无法创建一个抽象类的实体

public:

    virtual void draw() const = 0;     //被任何继承了它的具象类重新声明。声明一个纯虚函数是为了让派生类只继承函数接口。告诉继承者:我不管你怎么实现,必须有一个draw函数

    virtual void error(const string& msg);  //普通虚函数会提供一份实现代码,派生类可能覆写它。

    int objectID() const;

};


class Rectangle: public Shape{};

class Ellipse: public Shape{};


Shape* ps = new Shape;      //错误!是个抽象类

Shape* ps1 = new Rectangle;

ps1->draw();

Shape* ps2 = new Ellipse;

ps2->draw();

ps1->Shape::draw();         //调用Shape::draw()

ps2->Shape::draw();         //调用Shape::draw()


(2)普通虚函数:

class Airport{...};

class Airplane{

public:

    virtual void fly(const Airport& destination);

};


void Airplane::fly(const Airport &destination)

{

    缺省代码

}


class ModelA: public Airplane{...}; //如果派生类忘记重新定义其fly函数,就会调用基类的那个。有没有方法让派生类忘不了?

class ModelB: public Airplane{...};


(3)如果派生类忘记重新定义其fly函数,就会调用基类的那个。有没有方法让派生类忘不了?那就是写成纯虚函数

class Airport{...};

class Airplane{

public:

    virtual void fly(const Airport& destination) = 0; //由于是纯虚函数,派生类必须要实现

protected:

    void defaultFly(const Airport& destination);    //为什么protected:因为是派生类的实现细目。 必须是非虚函数,不能被覆写


};

void Airplane::defaultFly(const Airport &destination)

{

    缺省代码,将飞机飞至指定目的地。

}


class ModelA: public Airplane//派生类如果想用缺省实现,就在其fly函数中调用defaultFly()inline调用。

public:

    virtual void fly(const Airport& destination)

    {

        defaultFly(destination);

    }

};

class ModelB: public Airplane{

public:

    virtual void fly(const Airport& destination)  //由于是纯虚函数,派生类也不可能忘记了

    {

        defaultFly(destination);

    }

};


又有人说函数名称太雷同会污染命名空间??也有办法改!

class Airport{...};

class Airplane{

public:

    virtual void fly(const Airport& destination) = 0;   //fly被分成了两个基本要素:声明部分表现出接口(派生类必须使用),定义部分表现出缺省行为(派生类可能使用)

};

void Airplane::Fly(const Airport &destination)  //纯虚函数的函数实现。 纯虚函数也能拥有自己的定义

{

    缺省代码,将飞机飞至指定目的地。

}


class ModelA: public Airplane//派生类如果想用缺省实现,就在其fly函数中调用defaultFly()inline调用。

public:

    virtual void fly(const Airport& destination)

    {

        Airplane::Fly(destination);

    }

};

class ModelB: public Airplane{

public:

    virtual void fly(const Airport& destination)  //由于是纯虚函数,派生类也不可能忘记了

    {

        Airplane::Fly(destination);

    }

};


(4) non-virtual函数

非虚函数意味着它并不打算再派生类中有不同的行为。

声明非虚函数是为了令派生类继承函数的接口已经一份强制性实现。


(5)总结:


pure virtual 函数只具体指定接口继承。


简朴的(非纯) impure virtual 函数具体指定接口继承及缺省实现继承。


non-virtual函数具体指定接口继承以及强制性实现继承。


条款35 考虑virtual函数以外的其他选择 (替换虚函数的其他方式) 


我们一个游戏软件的血量计算方法为例来讲解这个主题,我们在游戏中有不同的游戏人物,不同游戏人物的血量计算方式不同,这驱使我们写出这样的面向对象代码。


class GameCharacter{

public:

    virtual int healthValue() const;

};


这里我们的实现是public的virtual形式。


这么一个简单的实现可以延伸出很多不同但既有优点又有缺点的做法:


(1)借助non-virtual Interface(NVI)手法实现Template Method(一种设计模式)

其实就是在虚函数外面包裹了一层调用。

缺点是非成员函数无法访问class的non-public成员。


class GameCharacter{

public:

    int healthValue() const             //派生类不再重新定义这个函数

    {

        ...

        int retVal = doHealthValue();   //做真正的工作

        ...

        return retVal;

    }

private:

    virtual int doHealthValue() const   //派生类可以重新定义它

    {

        ... //缺省算法。计算健康值

    }

};


这种方法我们通过public的非虚函数调用其私有的虚函数来实现,允许在多态调用前,进行一些设置工作,有一些特化的功能交给派生来处理。

这种方法就是模板方法。这种方法其实没有摆脱虚函数,只是加了一层封装而已。


(2)借助 函数指针以及函数对象 实现的strategy模式。


class GameCharacter;    //前置声明


int defaultHealthCalc(const GameCharacter& gc);    //计算健康值的缺省算法


class GameCharacter{

public:

    typedef int (*healthCalcFunc)(const GameCharacter&);

    

    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf)

    {}

    

    int healthValue() const

    {

        return healthFunc(*this);

    }

private:

    healthCalcFunc healthFunc;

};


class EvilBadGuy: public GameCharacter{

public:

    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hfc)

    {...}

    ...

};


int loseHealthQuickly(const GameCharacter&);    //函数1

int loseHealthSlowly(const GameCharacter&);     //函数2


EvilBadGuy ebg1(loseHealthQuickly); //相同人物类型的不同实体可以有不同的健康值函数

EvilBadGuy ebg2(loseHealthSlowly);  //已知人物之健康函数可以运行期变更


这种方式的好处的有两个:

1.同一个人物类型之不同实体可以有不同的健康计算函数。

2.同一个实体的健康计算函数也可以在运行期改变。通过设置健康计算函数。


但是随之而来也产生了一些问题,比如首先这个外部提供的函数可能无法访问私有变量,如果通过一些函数访问私有变量,就降低了类的封装性。


(3) 藉由 tr1::function 完成 Stratagy模式


利用函数对象实现的strategy方式就提供了更佳的弹性:可以用所用的可调用对象对其复制,可进行一些转换。


class GameCharacter;    //前置声明

int defaultHealthCalc(const GameCharacter& gc);    //计算健康值的缺省算法

class GameCharacter{

public:

    typedef std::tr1::function<int (const GameCharacter&)> healthCalcFunc;  //可调用物的要求是   参数可被隐式转换为const GameCharacter&

                                                                            //                而返回值可被隐式转换为int

    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf)

    {}

    

    int healthValue() const

    {

        return healthFunc(*this);

    }


private:

    healthCalcFunc healthFunc;

};


short calcHealth(const GameCharacter&); //健康计算函数


struct HealthCalculator{    //为计算健康而设计的 函数对象

    int operator() (const GameCharacter&) const

    {...}

};


class Gamelevel{

public:

    float health(const GameCharacter&) const;

    ...

};


class EvilBadGuy: public GameCharacter{

public:

    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hfc)

    {...}

    ...

};


class EyeCandyCharacter:public GameCharacter{   //EvilBadGuy类似的人物类

    ...

};


//下面是tr1::function允许做的三种事


EvilBadGuy ebg1(calcHealth);    //返回的short隐式转换为int


EyeCandyCharacter ecc2(HealthCalculator()); //使用某个 函数对象 计算


Gamelevel currentLevel;

...

EvilBadGuy ebg2(std::tr1::bind(&Gamelevel::health, currentLevel, _1));   //使用某个成员函数


(4)古典的strategy设计模式。

2017-12-19 下午2.52 拍摄的照片.jpg

将继承体系内的virtual替换为另一个继承体系内的virtual函数。

class GameCharacter;    //前置声明


class HealthCalcFunc{ //健康值计算类中的基类。可以派生出多重健康值计算方式 给人物类来使用。

public:

    ...

    virtual int calc(const GameCharacter& gc) const

    {...}

    ...

};


HealthCalcFunc defaultHealthCalc;


class GameCharacter{

public:

    explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc): pHealthCalc(phcf){}

    

    int healthValue() const //healthValue就可以不是virtual。

    {

        return pHealthCalc->calc(*this);

    }

private:

    HealthCalcFunc* pHealthCalc;

};


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

(0)non-virtual函数是静态绑定,virtual是动态绑定

class B{

public:

    void mf()

    {

        cout<<"B::mf()"<<endl;

    }

};


class D: public B{};


int main()

{

    D x;

    B* pB = &x;

    pB->mf();   //B::mf()

    D* pD = &x;

    pD->mf();   //B::mf()

    return 0;

}


  1. 同一对象的不同的指针调用的函数行为表现的不一致。
  2. 这还是浅层次理解,我们说了基类的函数被声明为non-virtual函数证明我们要继承函数的接口以及实现,所以我们不应该重新的non-virtual函数。
  3. (1)

class B{

public:

    void mf()

    {

        cout<<"B::mf()"<<endl;

    }

};


class D: public B{

public:

    void mf()   //遮掩掉了基类的所有mf()

    {

        cout<<"D::mf()"<<endl;

    }

};


int main()

{

    D x;

    B* pB = &x;

    pB->mf();   //B::mf()

    D* pD = &x;

    pD->mf();   //D::mf()   由于都是静态绑定,所以指针是什么类型,就会调用该类型的非虚函数

    return 0;

}


(2)如果mf是个virtual函数,无论是pb或是pd调用mf,都是调用D::mf,因为真正指的都是一个类型为D的对象。

class B{

public:

    virtual void mf()

    {

        cout<<"B::mf()"<<endl;

    }

};


class D: public B{

public:

    virtual void mf()   

    {

        cout<<"D::mf()"<<endl;

    }

};


int main()

{

    D x;

    B* pB = &x;

    pB->mf();   //D::mf()

    D* pD = &x;

    pD->mf();   //D::mf()   由于虚函数是动态绑定,所以对象是什么类型,就会调用该类型的虚函数

    return 0;

}


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

(1)

首先我们明确Non-virtual函数不应该被重新定义,所以这个条款的意思就是不要重新定义虚函数的缺省参数值

原因在于:虚函数是动态绑定的,但是缺省参数却是静态绑定的

如果缺省参数不是静态绑定,那么c++在这个问题的处理上又要像虚函数的设计一样,牺牲一定的效率。这个问题再具体一点就是,在动态绑定的时候我们需要继承基类的缺省参数值


class Shape{    //抽象类

public:

    enum ShapeColor{Red, Green, Blue};

    virtual void draw(ShapeColor color = Redconst = 0;

};


class Rectangle: public Shape{

public:

    virtual void draw(ShapeColor color = Greenconst//赋予不同的缺省参数值,很糟糕!!

    ...

};



class Circle: public Shape{

public:

    virtual void draw(ShapeColor color) const;

};


//对象的动态类型是指 目前所指对象的类型

Shape * ps ;                //静态类型为shape*,没有动态类型

Shape * pc = new Circle;    //静态类型为shape*,动态类型是Circle

Shape * pr = new Retangle//静态类型为shape*,动态类型是Retangle


//动态类型在程序执行过程中可以改变

ps = pc;

ps = pr;


//virtual函数是动态绑定而来,所以virtual函数调用取决于对象的动态类型

pc->draw(Shape::Red);   //调用Circle::draw(Shape::Red)

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

pr->draw();             //调用Retangle::draw(Shape::Red),而不是GREEN!因为缺省参数是静态绑定的,跟随指针的类型!


所以再次强调,绝不重新定义继承而来的缺省参数值,因为缺省参数值是静态绑定的,而虚函数--你唯一需要覆写的东西--却是动态绑定的。


(2) 可用 NVI 方式(条款35)修改:

class Shape{   

public:

    enum ShapeColor{Red, Green, Blue};

    void draw(ShapeColor color = Redconst

    {

        dodraw(color);

    }

private:

    virtual dodraw(ShapeColor color) const = 0;

};


class Rectangle: public Shape{

private:

    virtual void dodraw(ShapeColor color) const;

};


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

(0)复合和public继承完全不一样。

复合主要表明的意思是 has-a 以及 根据某物实现。


(1)在应用域类复合塑造出的关系式 has-a 比如Person类has-a name 并且has-a address

class Address{...};

class Phonenumber{...};


class Person{

public:

    ...

private:

    string name;

    Address addr;

    Phonenumber number;

};


(2)但是在实现域复合又表现出 根据某物实现出 的意思,

比如我们可以根据deque类实现出stack以及queue或者 list实现set 这就是 根据某物实现出 的道理。


template<class T>

class Set{

public:

    bool member(const T& item) const;

    void insert(const T& item);

    void remove(const T& item)l;

    size_t size() const;

private:

    list<T> rep;

};


(3) has-a 或者 根据某物实现 千万不要写成了继承的关系。


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


首先必须强调的一点是private继承不是Is-a关系,而是 根据某物实现,而 根据某物实现的最佳方式是复合(稍后我们会说private继承的用途)。


(1) 第一个使用private继承我们需要强调的一点是:编译器不会将一个private继承而来的对象转换成一个base类对象。我们首先来看一个例子:

class Person{};

class Student: private Person{};

void eat(Person&);

Person p;

Student s;

eat(p);//正确

eat(s);//错误,不会把一个private继承的派生类对象转换成一个基类对象


所以private继承只是一种实现手法并且表明的意义是根据某物实现,我们在 实现的时候尽量使用复合的手段来解决根据某物实现的问题


(2)

有的时候我们可能需要private继承:

1.像访问基类对象的protected成员,注意如果是复合的方式我们是没法访问的(其实这个是否可以用友元来替代)

2.需要重新定义基类class的某一个virtual函数。这个时候我们使用private继承。(当然这个也有适当的处理方法)


我们先来看需要重新定义一个virtual 函数的情况。

比如我们有一个widgets类需要定时做一些统计工作,我们完全可以使用private继承。比如下面这个timer函数


class Timer

{

public:

    explicit Timer(int tickFrequency);

    virtual onTick() const;

};


class widgets: private Timer

{

private: //private继承的虚函数就放在private里面,免得误导客户端以为能够被调用

    virtual onTick() const;

};


当然我们也有替代的方案(复合)如下:


class widgetTimer:public Timer{

public:

    virtual void onTick() const;

};

class widgets{

private:

    widgetTimer timer;

};


这种做法可以 1、阻住widget的派生类再重新定义虚函数onTick();  2、将widget编译依存性降至最低


(3)

总结:当你面对并不是is-a关系的两个class,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual函数,private继承极有可能成为正统设计策略。


(4)private继承而来的所有成员,在派生类中都变为private属性

最后强调一次,根据某物实现的最佳做法是复合并不是private继承,private继承的使用需要特别小心。


条款40 明智而谨慎地使用 多重继承


(0)多重继承: 继承一个以上的基类


(1)如果两个基类中有同名的函数(参数都不需要一样),就会出错!

class A{

public:

    void func()

    {

        cout<<"A::func()"<<endl;

    }

};


class B{

public:

    void func()

    {

        cout<<"B::func()"<<endl;

    }

};


class C : public A, public B{

};



int main()

{

    C c;

    c.B::func(); //如果遇到这种情况,必须写清楚用的哪个基类的函数,不然编译不过

    return 0;

}


(2)菱形继承

使用多重继承的时候一定要注意,两个基类是否有相同的基类,这样需要使用虚继承,但是虚继承又不得不带来新的问题就是增加对象的大小,速度以及初始化复杂度成本。


class A{};

class B : virtual public A{}; //如果没有virtual,基类的成员变量经过每条路径都被复制一次。

class C : virtual public A{};

class D : public B, public C{};


(3)

一般使用virtual class最好是不带任何数据。不过多重继承也有他的用途,比如我们可以多重继承来完成所谓的公有继承继承接口Private继承用于协助实现。


1、非必要不使用virtual bases。平常使用non-virtual继承。

2、如果必须使用virtual bases,不要在其中放数据。


多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。


(4)多重继承的确有正当用途。其中一个情节涉及“public继承某个 Interface class ” 和 “private继承某个协助实现的class”的组合。

class IPerson{

public:

    virtual ~IPerson();

    virtual string name() const = 0;

    virtual string birthDate() const = 0;

};


class DatabaseID{...};


class PersonInfo{       //这个类有若干有用的函数,可用于实现IPerson接口。

public:

    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{      //多重继承。CPerson是一个IPerson,可以根据PersonInfo(想用这个类里面的函数)来实现。

public:

    explicit CPerson(DatabaseID pid): PersonInfo(pid){} //把参数传给基类来初始化

    

    virtual string name() const                         //实现必要的IPerson成员函数

    {

        return PersonInfo::theName();

    }

    

    virtual string birthDate() const                    //实现必要的IPerson成员函数

    {

        return PersonInfo::theBirthDate();

    }

    

private:

    const char* valueDelimOpen() const {return "";}

    const char* valueDelimClose() const {return "";}

};


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值