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

Posted NearXDU

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Effective C++笔记—继承与面向对象设计相关的知识,希望对你有一定的参考价值。


条款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() = 0cout << "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 Filepublic: 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 Filepublic: 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中的某些虚函数方法。

以上是关于Effective C++笔记—继承与面向对象设计的主要内容,如果未能解决你的问题,请参考以下文章

effective C++ 读书精华笔记提取

Item 41:隐式接口与编译期多态 Effective C++笔记

Effective C++ 笔记:4设计与声明

Item 39:明智地使用private继承 Effective C++笔记

Java:Effective java学习笔记之 复合优先于继承

《Effective C++》读书笔记汇总