Effective C++笔记(8)—继承与面向对象设计

来源:互联网 发布:网上商城源码 java 编辑:程序博客网 时间:2024/06/02 04:46

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

以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味着”is-a”关系。

class Person{};class Student:public Person{};

表示Student is-a Person

但是,公有继承is-a关系,需要注意对接口的设计。

书中的例子:

class Bird{};class Penguin:public Bird{};

Penguin is-a Bird 但是企鹅不会飞。
再比如:

class Rectangle{};class Square : public Rectangle{};

我们知道正方形是矩形,但在设计时,很可能在Rectangle类中有一个makeBigger函数,他会修改矩形的长或者宽,当它被继承到Square中后,显然是不合理的。


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

int main(){    int x = 10;    {        double x = 5.5;        cout << x << endl;//输出5.5    }    return 0;}

到类继承关系中,我的理解编译器遵循就近原则,比如上述例子中,先找到local再到全局,全局的同名变量就被local遮盖了。

class Base{private:    int x;public:    virtual void mf1() = 0;    virtual void mf1(int a){ cout << "base::mf1::int" << endl; }    virtual void mf2() { cout << "Base::mf2" << endl; }    void mf3(){ cout << "base::mf3" << endl; }    void mf3(double a){ cout << "base::mf3::double" << endl; }};class Derived :public Base{public:    virtual void mf1(){ cout << "derived::mf1()"<<endl; }    void mf3(){ cout << "derived::mf3" << endl; }    void mf4(){ mf2(); }};int main(){    Derived d;    d.mf1();// ok   derived::mf1()    //d.mf1(2);//error 继承类中的mf1遮盖了Base    d.mf2();//ok public继承直接调用base  base::mf2    d.mf3();//ok 调用derived版本 base被遮盖  derived::mf3    //d.mf3(2);//error 理由同上    getchar();    return 0;}

使用using可以让Derived看见base里面同名的内容,而不至于遮盖:

class Derived :public Base{public:    using Base::mf1;    using Base::mf3;    virtual void mf1(){ cout << "derived::mf1()"<<endl; }    void mf3(){ cout << "derived::mf3" << endl; }    void mf4(){ mf2(); }};int main(){    Derived d;    d.mf1();// ok   derived::mf1()    d.mf1(2);//现在ok了  base::mf1::int    d.mf2();//ok public继承直接调用base  base::mf2    d.mf3();//ok 调用derived版本 base被遮盖  derived::mf3    d.mf3(2);//现在ok了  base::mf3::double    getchar();    return 0;}

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

本条款收获的几个知识:
1.纯虚函数也可以有实现,如果给虚基类的析构函数定义成纯虚函数,则必须要给一个实现,否则会出现链接错误。
2.在多态环境下,函数和接口的继承有3种情况:

纯虚函数:要求派生类必须实现自己的版本,即只继承接口
虚函数:派生类可以有自己的实现版本,既继承了接口,又继承了默认版本(基类版本)
普通成员函数:接口继承和强制性的实现继承,不具备运行时类型检查,会调用基类版本。

公有继承之下,派生类总是继承基类的接口。

#include <iostream>#include <string>using namespace std;class Shape{public:    virtual void draw() = 0{cout << "shape::draw" << endl;}    virtual void error(){ cout << "Shape::error" << endl; }    void objectID(){ cout << "Shape::ObjectID" << endl; }    virtual ~Shape() = 0{}};class Rectangle : public Shape{public:    virtual void draw(){cout << "Rectangle::draw" << endl;}    //virtual void error(){ cout << "Rectangle::error" << endl; }    void objectID(){ cout << "Rectangle::ObjectID" << endl; }};class Ellipse : public Shape{public:    virtual void draw(){ cout << "Ellipse::draw" << endl;}    virtual void error(){ cout << "Ellipse::error" << endl; }    void objectID(){ cout << "Ellipse::ObjectID" << endl; }};int main(){    //Shape *ps = new Shape;//error  Shape是抽象类类型    Shape* ps1 = new Rectangle;    ps1->draw();//OK  Rectangle::draw    Shape *ps2 = new Ellipse;    ps2->draw();//OK   Ellipse::draw    ps1->Shape::draw();// OK shape::draw    ps2->Shape::draw();//OK  shape::draw    ps1->error();//ok 非纯虚函数继承了接口和默认版本  shape::error    ps1->objectID();//ok 强制继承了基类的实现  shape::objectID    delete ps1;    delete ps2;    system("pause");    return 0;}

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

(一)Non-Virtual Interface(NVI)手法实现Template Method 模式

多态环境中,使用private的virtual函数,和non-virtual的public接口,这样,在public接口中调用virutal函数,同样可以达到多态的效果,并且可以控制在调用virtual函数前和调用virtual函数后执行的动作。

class GameCharacter{public:    void healthValue()const    {        ///before:  加锁(mutex)、日志记录(log)等        doHealthValue();        ///after:   解锁(unmutex)等    }private:    virtual void doHealthValue()const    {        cout << "GameCharacter::doHealthValue" << endl;    }};class A :public GameCharacter{private:    virtual void doHealthValue()const    {        cout << "A::doHealthValue" << endl;     }};int main(){    GameCharacter *ps = new A;    ps->healthValue();//OK   A::doHealthValue    delete ps;    system("pause");    return 0;}

(二)Stratege模式

书中给出了三种实现stratege模式的方式。
关于什么是stratege模式,参考:
http://blog.csdn.net/lihao21/article/details/48008953
Strategy模式是一种行为型设计模式,它将算法一个个封装起来,在某一时刻能够互换地使用其中的一个算法。从概念上看,所有这些算法完成的都是相同的工作,只是实现不同而已。

2.1古典的Strategy模式

借由虚函数实现,比如书中的例子,在游戏设计中,好的NPC和坏的NPC有不同的计算血量的方式。

我们将计算血量这个行为封装成class,并将calc设定为虚函数,这样,就可以派生出不同的计算血量的class他们对应不同的行为。

下面代码实现书中UML图所示

#include <iostream>#include <string>using namespace std;class HealthCalc{public:    virtual void calc() const    {        cout << "HealthCalc::calc" << endl;    }};class Fast :public HealthCalc{public:    virtual void calc() const    {        cout << "Fast::calc" << endl;    }};class Slow :public HealthCalc{public:    virtual void calc() const    {        cout << "Slow::calc" << endl;    }};HealthCalc defaultCalc;class GameCharacter{public:    explicit GameCharacter(HealthCalc *p=&defaultCalc) :pHealthCalc(p){}    void healthValue()    {        pHealthCalc->calc();    }    void setMethod(HealthCalc *p)    {        pHealthCalc = p;    }private:    HealthCalc * pHealthCalc;};class EvilBadGuy : public GameCharacter{public:    explicit EvilBadGuy(HealthCalc *p) :GameCharacter(p){}};class EyeCandyCharacter : public GameCharacter{public:    explicit EyeCandyCharacter(HealthCalc *p) :GameCharacter(p){}};int main(){    HealthCalc * p1 = new Fast;    HealthCalc*p2 = new Slow;    GameCharacter gc;    gc.healthValue();//ok   HealthCalc::calc    gc.setMethod(p1);    gc.healthValue();//ok   Fast::calc    gc.setMethod(p2);    gc.healthValue();//ok    Slow::calc//.............................//    EvilBadGuy badGuy(p1);    badGuy.healthValue();//ok  Fast::clac    EyeCandyCharacter eye(p2);//ok Slow::clac    eye.healthValue();    delete p1;    delete p2;    system("pause");    return 0;}

2.2使用Function Pointer实现Strategy模式

函数指针可以绑定不同的策略函数。
借由上面同样的例子,可以做如下修改,将类改成函数,并经由函数指针来调用:

void defulatCalc(){    cout << "defalutCalc()" << endl;}void fastCalc(){    cout << "fastCalc()" << endl;}void slowCalc(){    cout << "slowCalc()" << endl;}class GameCharacter{public:    typedef void(*pHeathFunc)(void);    explicit GameCharacter(pHeathFunc p = defulatCalc) :healthFunc(p){}    void healthValue()    {        healthFunc();    }private:    pHeathFunc healthFunc;};class EvilBadGuy : public GameCharacter{public:    explicit EvilBadGuy(pHeathFunc p) :GameCharacter(p){}};class EyeCandyCharacter : public GameCharacter{public:    explicit EyeCandyCharacter(pHeathFunc p) :GameCharacter(p){}};int main(){    EvilBadGuy badGuy(fastCalc);    EyeCandyCharacter eyeCandy(slowCalc);    badGuy.healthValue(); // fastCalc()    eyeCandy.healthValue();// slowClac()    system("pause");    return 0;}

2.3使用tr1::function实现Strategy模式

function类似于指针,但是不强调绝对的返回值和参数类型,他是一个兼容模式,这里的兼容模式我理解为调用形式一致,借由bind可以进行参数绑定:

//这里pFunc不仅仅是参数为int返回void的函数指针,他是一个强大的兼容模式。typedef std::tr1::function<void()> pFunc;void fun1(int i){    cout << i << endl;}void func2(int i, int j, int k){    cout << i + j + k << endl;}int main(){    pFunc p1 = std::tr1::bind(fun1, 20);    p1();// 1    pFunc p3 = std::tr1::bind(func2, 1,2,3);    p3();//6    pFunc p4 = std::tr1::bind(func2, 2, 20, 20);    p4();//42    system("pause");//    return 0;}

在刚刚的例子中,用function替代函数指针

void defulatCalc(){    cout << "defalutCalc()" << endl;}void fastCalc(){    cout << "fastCalc()" << endl;}void slowCalc(){    cout << "slowCalc()" << endl;}class GameCharacter{public:    typedef tr1::function<void(void)>HealthCalcFunc;    explicit GameCharacter(HealthCalcFunc p = defulatCalc) :healthFunc(p){}    void healthValue()    {        healthFunc();    }private:    HealthCalcFunc healthFunc;};class EvilBadGuy : public GameCharacter{public:    explicit EvilBadGuy(HealthCalcFunc p) :GameCharacter(p){}};class EyeCandyCharacter : public GameCharacter{public:    explicit EyeCandyCharacter(HealthCalcFunc p) :GameCharacter(p){}};class Foo{public:    void FooCalc(int i)    {        cout << "Foo::FooCalc" << endl;        cout << i << endl;    }};int main(){    EvilBadGuy badGuy(fastCalc);    EyeCandyCharacter eyeCandy(slowCalc);    badGuy.healthValue(); // fastCalc()    eyeCandy.healthValue();// slowClac()    Foo foo;    EvilBadGuy anotherBadGuy(tr1::bind(&Foo::FooCalc,        &foo,42));    anotherBadGuy.healthValue();// Foo::FooCalc  42    system("pause");    return 0;}

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

重写父类的继承而来的虚函数,目的在于实现多态,使用父类指针调用该函数时在运行时动态绑定特定版本。

本条款强调非虚函数在子类中不要重写:
1.子类中的同名函数会遮盖父类版本。
2.使用父类指针调用该函数达不到多态效果。

非虚函数都是静态绑定的,也即编译期决定了,不具备运行时动态绑定效果。

class Base{public:    void mf()    {        cout << "base::mf" << endl;    }};class Derived :public Base{public:    void mf()    {        cout << "Derived::mf" << endl;    }};int main(){    Derived d;    d.mf();//Derived::mf    d.Base::mf();//Base::mf    Base * pb = &d;    pb->mf();//Base::mf    Derived *pd = &d;    pd->mf();//Derived::mf    system("pause");    return 0;}

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

本条款的前提是,继承一个virtual函数,但这个virtual函数带有缺省参数值。这就非常有意思了,virtual函数是运行时绑定的,而缺省参数值是在编译期决定的静态绑定。

class Shape{public:    enum ShapeColor{ Red, Green, Blue};    virtual void draw(ShapeColor color = Blue) const = 0;};class Rectangle :public Shape{public:    //赋予不同的缺省参数、真糟糕!    virtual void draw(ShapeColor color = Green) const    {        cout << "Rectangle::draw" << endl;        cout << color << endl;    }};class Circle :public Shape{    //调用时需要指定参数    //静态绑定下不从base继承缺省参数值    //但若以指针或引用来调用则可以不指定    //因为动态绑定这个函数会继承base的缺省值。public:    virtual void draw(ShapeColor color) const    {        cout << "Circle::draw" << endl;        cout << color << endl;    }};int main(){    Circle c;    //c.draw();//error 调用的参数太少    c.draw(Shape::Green);// OK  Circle::draw 1    Shape *pc = &c;    //ok base的缺省参数被继承下来    pc->draw(); //  circle::draw   2    Rectangle r;    Shape*pr = &r;    pr->draw();//rectangle::draw 2    system("pause");    return 0;}

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

has-ais-implemented-in-terms-of
这两种情况分别举例:
has-a复合关系:

class Address{};class Name{};class Person{    private:        Address a;        Name n;};

is-implemented-in-terms-of复合关系
例如想通过STL标准容器list作为底层container实现一个Set

template <class T>class Set{    public:        void insert(const T& t);    private:        std::list<T> rep;};template <class T>void Set<T>::insert(const T& t){    if(!member(t))//set中元素互斥        rep.push_back(t);}

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

先通过一个例子来看看

class Person{};class Student :private Person{};void eat(const Person &p){    cout << "eat" << endl;}int main(){    Person p;    Student s;    eat(p);    //eat(s);//error 不允许对不可访问的基类进行转换    //Person *pp = &s;//error 不允许对不可访问的基类进行转换    system("pause");    return 0;}

private继承的两条规则:
1.编译器不会自动将一个derived class对象转换成一个base class对象
2.在基类中的继承而来的所有成员属性都会变成Private

private继承意味着is-implemented-in-terms-of表示经由某某实现,跟上述条款38“复合”类似。

私有继承纯粹是一种实现技术,基类的实现被继承下来,接口却没有。

怎么理解,私有继承后,基类中的成员在派生类中变成了private,这样派生类的对象无法通过基类的接口进行调用,然而派生类却可以在自己的公有接口实现中调用基类的protected和public的函数。

class Base{public:    void func1(){ cout << "func1" << endl; }    void func2(){ cout << "func2" << endl; }protected:    void func3(){ cout << "func3" << endl; }};class Derived :private Base{public:    void func4()    {        //借由基类中方法实现的部分        func1();        func2();        func3();        //自己的实现        cout << "func4" << endl;    }};int main(){    Derived d;//  d.func1();//error 不可访问    d.func4();//ok  func1 func2 func3 func4    system("pause");    return 0;}

这个例子中Derived class中的func4借由继承而来的func1~func3的实现完成,而Derived的对象无法通过func1接口调用(私有继承后不可访问Private)。

可以看到:private继承意味着只有实现部分被继承,接口部分应省略,称之为implemented-in-terms-of

另外这里还提到了一个作用,叫做EBO(empty base optimization)

我们知道sizeof一个空类得到的大小是1,这是编译器将默默安插一个char,表示这个类的存在。如下代码由于字节对齐,可能会耗费更多的内存:

class empty{};class hold{    private:        int x;        empty e;};sizeof(hold);//8 

空基类优化指的是让派生类继承这个基类:

class hold :private empty{    private:        int x;}sizeof(hold);//4 

至于为什么要使用EBO空基类优化,参照:
C++ EBO空基类优化


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

多重继承可能带来歧义,看下面的例子:

class BorrowableItem{public:    void checkOut(){}};class ElectronicGadget{private:    bool checkOut(){}};class MP3 : public BorrowableItem,    public ElectonicGadget{  };int main(){    MP3 mp;    mp.checkOut();//error checkOut不明确    mp.BorrowableItem::checkOut();//ok    getchar();    return 0;}

虽然ElectronicGadget中的checkOut并不能访问,不过C++是首先确认这个函数对此调用之言是最佳匹配。因此private checkOut就未能被编译器通过。

在多重继承中,如果存在base class不出现在最高级base class,则出现了“钻石型多重继承”,在我以前的学习过程中,这被称为菱形继承。例如:

class File{public: int file; };class Input : public File{};class Output : public File{};class IO :public Input, public Output{};int main(){    IO io;    io.file = 0;//file 不明确    getchar();    return 0;}

这是有问题的,因为成员变量可以经由两条路径被复制,这样最终到了最底层的派生类之后就变得不明确。

解决的办法是使用virtual继承:

class File{public: int file; };class Input : virtual public File{};class Output : virtual public File{};class IO :public Input, public Output{};int main(){    IO io;    io.file = 42;//OK    cout << io.file << endl;    getchar();    return 0;}

但是,virtual继承需要付出一定的代价:
1.virtual 继承的那些classes所产生的对象往往比使用non-virtual的大
2.访问virtaul base class的成员变量时也比non-virtual base class慢。

书中最后给出了一个使用多重继承的好处

比如说,伪代码如下:

IPerson//接口IPerson & makePerson(DataBaseID id);DataBaseID id;IPerson & pp = makePerson(id)

可以想见,通过makePerson这个工厂函数来创建Person对象,这些Person类都继承自IPerson。

假设现在要写一个CPerson类,我们可以不必从头写起,我们可以经由一个工具类PersonInfo来实现is-implemtented-in-terms-of,但是PersonInfo不一定完全符合我们的条件,这个时候,需要在CPerson类中进行重写虚方法。

这就是多重继承的例子,他的继承体系是一个多重继承。

IPerson//接口PersonInfo//工具类包含一些需要虚函数方法CPerson :public IPerson,private PersonInfo//在CPerson根据需求重写PersonInfo中的某些虚函数方法。
阅读全文
1 0