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

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

条款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中的某些虚函数方法。
阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页