Effective C++笔记⑥

继承和面向对象设计

面向对象编程(OOP)几乎已经风靡了两个年代了,所以关于继承、派生、virtual函数等等。尽管如此,C++的OOP有可能和你原本习惯的OOP稍有不同:“继承”可以是单一继承或多重继承,每一个继承连接(link)可以是public,protected或private,也可以是virtual或non-virtual。本章节需要理解的问题:

  1. 成员函数的各个选项:virtual? non-virtual? pure virtual?
  2. 缺省参数值与virtual函数有什么交互影响?
  3. 继承如何影响C++的名称查找规则?
  4. 设计选项有哪些?
  5. 如果class的行为需要修改,virtual函数是最佳选择吗?

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

C++对于public继承,如下:

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

根据生活经验我们知道,每个学生都是人,但并非每个人都是学生。这边是这个继承体系的主张。我们预期,对人可以成立的每一件事,对学生来说也都成立。

于是,在C++领域中,任何函数如果期望获得一个类型为Person(指针或引用)的实参,都也愿意接受一个Student对象(或指针或引用):

void ear(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);
eat(s);
study(s);
study(p);        //错误,并非每个人都会学习

这个论点只对public继承才成立。只有当Student以public形式继承Person,C++的行为才会如我所描述。private继承的意义与此完全不同(见条款39),至于protected继承......

public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子,企鹅是一种鸟,鸟可以飞,但如果以C++的语言去描述这层关系,结果如下:

class Bird{
public:
    virtual void fly();
    ...
};

class Penguin:public Bird{
    ...
};

这个体系告诉我们,企鹅是鸟,所以它也能够飞,但我们知道那是绝不可能发生的。

我们可以将其改成会飞的鸟才能够继承飞这一特性:

class Bird { ... };

class FlyingBird: public Bird{
public:
    virtual void fly();
    ...
};

class Penguin: public Bird { ... };

又如下段代码:

class Rectangle{
public:
    virtual void setHeight(int newHeight);
    virtual void setWidth(int newWidth);
    virtual int height() const;
    virtual int width() const;
    ...    
};

void makeBigger(Rectangle& r)
{
    int oldHeight = r.height();
    r.setWidth(r.width() + 10);
    assert(r.height() == oldHeight);
}

显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度:r高度从未被改变。

现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:

class Square: public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());

显然,对于上述的判断均为真,毕竟正方形的边长都是一样的。

但现在我们遇上了一个问题。我们如何调节下面哥哥assert判断式:

  1. 调用makeBigger之前,s的高度和宽度相同;
  2. 在makeBigger函数内,s的宽度改变,但高度不变;
  3. makeBigger返回之后,s的高度再度和其宽度相同;

我们应该确定确实了解这些个“classes相互关系”之间的差异,并知道如何在C++中最好地塑造他们。

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

诸如以下代码:

int x;            //全局变量
void someFunc()
{
    double x;     //作用域内变量
    std::cin>>x;  //赋予x新值
}

这个读取数据的语句指涉的是local变量x,而不是global变量x,因为内层作用域的名称会遮掩外围作用域的名称。我们可以这么看本例作用域形势:

现在导入继承。我们知道,位于一个derived class成员函数内指涉base class内的某物(也许是某个成员函数、typedef、或成员变量)时,编译器可以找出我们所指涉的东西,因为derived classes继承了声明于base classes内的所有东西。实际运作方式是,derived class作用域被嵌套在base class作用域内,像这样:

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

此例内含private、public名称,以及一组成员变量和成员函数名称。这些成员函数包括pure virtual、impure virtual和non-virtual三种,这是为了强调我们谈的是名称,和其他无关。本例使用单一继承,然而一旦了解单一继承下发生的事,很容易就可以推想C++在多重继承下的行为。

假设derived class内的mf4的实现码像这样:

void Derived::mf4()
{
    ...
    mf2();
    ...
}

当编译器看到这里使用名称mf2,必须估算它指涉什么东西。编译器的做法是查找各作用域,看看有没有某个名为mf2的声明式。首先查找local,而后查找外围,也就是class Derived覆盖的作用域。本例为base class,在那儿编译器找到一个名为mf2的东西了,所以停止了查找。

再次考虑前一个例子,这次重载mf1和mf3,并且添加一个新版mf3到Derived去。如条款36所说,这里发生的事情是:Derived重载了mf3,那是一个继承而来的non-virtual函数。

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

以上代码中以作用域为基础的“名称遮掩规则”并没有改变,因此base class内素有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承。

Derived d;
int x;
...
d.mf1();
d.mf1(x);    //错误,Derived::mf1遮掩了Base::mf1
d.mf2();
d.mf3();
d.mf3(x);    //错误,Derived::mf3遮掩了Base::mf3

要解决上述问题,我们可以使用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;        //让Base class内名为mf1和mf3的所有东西
    using Base::mf3;        //在Derived作用域内都可见(并且public)
    virtual void mf1();
    void mf3();
    void mf4();
    ...
};

这意味着如果你继承base class并加上重载函数,而你又希望重新定义或复写其中一部分,那么你必须为那些原本会遮掩的每个名称引入一个using声明式,否则某些你希望继承的名称会被遮掩。

以上都是在public下进行的继承,但是如果我们以private形式继承,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里排不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中可见。我们仅仅需要一个简单的转交函数:

class Base{
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    ...
};

class Derived: private Base{
public:
    virtual void mf1() {    //转交函数
        Base::mf1();        //暗自成为inline(见条款30)
    }
    ...
};

...
Derived d;
int x;
d.mf1();    
d.mf1(x);    //错误,Base::mf1()被遮掩了

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

表面上是public继承概念,经过严密的检查之后,发现它由两部分组成:

  • 函数接口继承
  • 函数实现继承

为了更好地感觉上述选择之间的差异,我们考虑一个展现绘图程序中各种几何形状的class继承体系:

class Shape{
public:
    virtual void draw() const = 0;
    virtual void error(const std::string& msg);
    int objextID() const;
    ...
};

class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };

Shape是个抽象class,它的纯虚函数draw使他成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其派生类的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的派生类,因为:

  • 成员函数的接口总是会被继承。一如条款32所说,public继承意味着is-a,所以对基类为真的任何事情一定也对其派生类为真。因此如果某个函数可施行于某class上,一定也可施行于其派生类上。

对于上述代码中不同的声明,我们需要进行仔细的探讨:

class Shape{
public:
    virtual void draw() const = 0;
    ...
};

纯虚函数有两个突出的特性:

  1. 必须被任何“继承了它们”的具象class重新声明;
  2. 在抽象class中通常没有定义;

【注】声明一个纯虚函数的目的是为了让派生类只继承函数接口。

令人意外的是,我们可以为纯虚函数提供定义。也就是说可以为draw供应一份实现代码,C++并不会对此发出怨言,但调用它的唯一途径是“调用时明确指出其class名称”:

Shape* ps = new Shape;        //错误,Shape是抽象的
Shape* ps1 = new Rectangle;
ps1->draw();                  //调用Rectangle::draw
Shape* ps2 = new Ellipse;
ps2->draw();                  //调用Ellipse::draw
ps1->Shape::draw();           //调用Shape::draw
ps2->Shape::draw();           //调用Shape::draw

【注】声明简朴的非纯虚函数的目的,是让派生类继承该函数的接口和缺省实现。

考虑如下例子:

class Shape{
public:
    virtual void error(const std::string& msg);
    ...
};

其接口表示,每个class都必须支持一个“当遇上了错误可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以回退到Shape class提供的缺省错误处理行为。

最后,让我们看看Shape的非虚函数objectID:
 

class Shape{
public:
    int objectID() const;
    ...
};

如果成员函数是个non-virtual函数,意味是它并不打算在派生类中有不同的行为。实际上一个非虚成员函数所表现的不变性凌驾其特异性,因为它表示不论派生类变得多么特异化,它的行为都不可以改变。

【注】声明非虚函数的目的是为了令派生类继承函数的接口及一份强制性实现。

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

假设你正在写一个视频游戏软件,你打算为游戏内的人物设计一个继承体系。你的游戏属于暴力砍杀类型,剧中人物被伤害或因其他因素而降低健康状态的情况并不罕见。你因此决定提供一个成员函数healthValue,它会返回一个整数,表示人物健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将healthValue申明为virtual似乎是再明白不过的做法:

class GameCharacter{
public:
    virtual int healthValue() const;
    ...
};

但是上面的申明缺还是有明显的缺点。

藉由non-virtual interface手法实现Template Method模式

这一个流派主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:

class GameCharacter{
public:
    int healthValue() const
    {
        ...                                //事前工作
        int retVal = doHealthValue();      //真正工作
        ...                                //事后工作
        return retVal;
    }
    ...
private:
    virtual int doHealthValue() const
    {
        ...
    }
};

这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。就是Template Method设计模式的一个独特表现形式。这样一个手法的优点隐身于上述代码注释“事前/事后工作”中。能够告诉你的代码保证在“virtual函数进行真正工作之前和之后”被调用。

但是这有个需要注意的点:NVI手法设计在派生类内重新定义private virtual函数。“重新定义virtual函数”表示某些事“如何被完成,”调用virtual函数”则表示它“何时”被完成。各自相互独立。其实在NVI手法下没必要让virtual函数一定得是private。某些class继承体系要求派生类在virtual函数的实现内必须调用其基类的对应兄弟,而为了让这样的调用合法,virtual函数必须是protected,不能是private。

藉由Function Pointers实现Strategy模式

NVI手法对public virtual函数而言是一个有趣的替代方案,但从某种设计角度观之,它只比窗饰花样更强一些而已。毕竟我们还是使用virtual函数来计算每个人的健康指数。另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这个的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

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;
};

这个做法是常见的Strategy设计模式的简单应用。拿他和“植基于GameCharacter继承体系内之virtual函数”的做法不比较,它提供了某些有趣弹性:

同一人物类型之不同实体可以有不同的健康计算函数。例如:

class EvilBadGuy: public GameCharacter{
public:
    explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc) : GameCharacter(hcf)
    {
        ...
    }
    ...
};

int loseHealthQuickly(const GameCharacter&);    //健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);     //健康指数计算函数1

EvilBadGuy ebg1(loseHealthQuickly);             //相同类型的人物搭配
EvilBadGuy ebg2(loseHealthSlowly);              //不同的健康计算方式

某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

藉由tr1::function完成Strategy模式

一旦习惯了templates以及它们对隐式接口(见条款41)的使用,基于函数指针的做法看起来便过分苛刻和死板了。如果我们不再使用函数指针,而是改用一个类型为tr1::function的对象,这些约束就全都不见了。就如条款54所说,这样的对象可持有任何可调用物(也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。以下将刚才的设计改为使用tr1::functiion:

class GameCharacter;        //前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    //HealthCalcFunc可以是任何“可调用物”,可被调用并接受任何兼容
    //于GameCharacter之物,返回任何兼容于int的东西,详下。

    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
    {}
    
    int healthValue() const
    {
        return healthFunc(*this);
    }
    ...

private:
    HealthCalcFunc healthFunc;
};

上面的代码中,typedef处代表的函数是“接受一个引用指向const GameCharacter,并返回int”。这个tr1::function类型产生的对象可以持有任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可别被隐式转换为int。

古典的Strategy模式

传统的Strategy做法会将健康计算函数做成一个分离的继承体系中的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
    {
        return pHealthCalc->calc(*this);
    }
private:
    HealthCalcFunc* pHealthCalc;
};

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

假设Class D系由Class B以public形式派生而来,Class B定义有一个public成员函数mf。由于mf的参数和返回值都不重要,所以我假设两者皆为void。换句话说的意思是:

class B{
public:
    void mf();
    ...
};

class D: public B { ... };

同时我们做如下操作:

D X;

B* pB = &x;        //基类指针指向子类对象
pB->mf();

D* pD = &x;        //子类指针指向子类对象
pD->mf();

由于两者调用的函数相同,凭借的对象也相同,所以行为也相同吗?

答案是否认的。如果mf是个non-virtual函数而D定义有自己版本的mf函数,那就不是如此:

class D: public B{
public:
    void mf();
    ...
};
pB->mf();    //调用B::mf
pD->mf();    //调用D::mf

造成此一两面行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定。这意思是,由于pB被声明为一个指向B的指针,通过B调用的non-virtual函数永远是B定义的版本,即使pB指向一个类型为“B派生之class”的对象。

但如果mf是个virtual函数,当mf被调用,任何一个D对象都可能表现出B或D的行为:决定因素不在对象自身,而在于“指向该对象之指针”当初的声明类型。

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

一开始你只能继承两种函数:

  1. virtual函数;
  2. non-virtual函数;

然而重新定义一个继承而来的non-virtual函数永远都是错误的(见条款36),所以我们可以安全地将本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”。

所欲为的静态类型,就是它在程序中被声明时所采用的类型。考虑一下的class继承体系:

class Shape{
public:
    enum ShapeColor { Red, Green, Blue };
    //所有形状都必须提供一个函数,用来绘出自己
    virtual void draw(ShapeColor color = Red) const = 0;
    ...
};

class Rectangle: public Shape{
public:
    //注意,赋予不用的缺省参数值,这真的糟糕!
    virtual void draw(ShapeColor color = Green) const;
    ...
};

class Circle: public Shape{
public:
    virtual void draw(ShapeColor color) cosnt;
    //注意:以上这么写则当客户以对象调用此函数,一定要指定参数值
    //     因为静态绑定下这个函数并不从其base继承缺省参数值
    //     但若以指针(或引用)调用此函数,可以不指定参数值
    //     因为动态绑定下这个函数会从其base继承缺省参数值
};

现在考虑以下指针:

Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;

【注】上述都以Shape*为静态类型!

对象的动态类型则是指“目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。就以上而言,pc的动态类型就是Circle*,pr的动态类型就是Rectangle*。而ps没有动态类型,因为其没有指向任何对象。

动态类型如名称所示,可以在程序执行过程中改变(通常经由赋值动作)。

virtual函数系动态绑定而来,意思是调用一个虚函数时,酒精调用哪一份函数来实现代码,取决于发出调用的那个对象的动态类型:

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

同时,虚函数是动态绑定的,而缺省参数是静态绑定的。你可能在“调用一个派生类内的虚函数”时,调用基类为它指定的缺省参数值:

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

当你试着遵守这条规则,并且同时提供缺省参数值给基类和派生类用户,又会发生什么事呢?

class Shape{
public:
    enum ShapeColor { Red, Green, Blue };
    virtual void draw(ShapeColor color = Red) const = 0;
    ...
};

class Rectangle: public Shape{
public:
    virtual void draw(ShapeColor color = Red) const;
    ...
};

代码重复,且带着相依性:如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些派生类也不必须改变,苟泽它们最终会导致“重复定义一个继承而来的缺省参数值”。怎么办?

当想令虚函数表现出你所想要的行为但却遭到麻烦,聪明的做法是考虑替代设计。条款35列了不少virtual函数的替代设计,其中之一是NVI手法,令基类内的一个公有非虚函数调用私有虚函数,后者可被派生类重新定义。在这里,我们可让非虚函数指定缺省参数,而私有虚函数负责真正的工作:

class Shape{
public:
    enum ShapeColor { Red, Green, Blue };
    void draw(ShapeColor color = Red) const
    {
        doDraw(color);
    }
    ...
private:
    virtual void doDraw(ShapeColor color) const = 0;
};

class Rectangle: public Shape{
public:
    ...
private:    
    virtual void doDraw(ShapeColor color) const;    //不能指定缺省参数值
    ...
};

由于非虚函数绝对不被派生类复写(见条款36),这个设计很清楚地使draw函数的color缺省参数值总是为Red。

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

复合是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:

class Address { ... };
class PhoneNumber { ... };

class Person{
public:
    ...
private:
    std::string name;        //合成成分物
    Address address;         //同上
    PhoneNumber voiceNumber; //同上
    PhoneNumber faxNumber;   //同上
};

本例中,Person对象由string、Address、PhoneNumber构成。在程序员之间复合这个属于有许多同义词:

  • 分层(layering)
  • 内含(containment)
  • 聚合(aggregation)
  • 内嵌(embeding)

区分is-a(是一种)和is-implemented-in-terms-of(根据某物实现出)这两种对象关系是比较麻烦的。假设你需要一个template,希望制造出一组classes用来表现出不重复对象组成的sets。由于复用(reuse)是件美妙无比的事情,你的第一个直觉是采用不标准程序库提供的set template。

不幸的是,set的实现往往招致“每个元素耗用三个指针”的额外开销。因为sets通常以平衡查找树实现而成,使得在查找、安插、移除元素时保证拥有对数时间效率。

如果你是一位数据结构专家,就会知道实现set的方法太多了,其中一种便是在底层采用linked lists。而标准程序库有一个list template,于是可以复用它。更明确的说,你决定让你那个萌芽中的set template继承std::list。也就是Set<T>继承List<T>。于是,对Set template声明如下:

template<typename T>
class Set: public std::list<T> { ... };

但正如条款32所说,如果D是一种B,对B为真的每一件事对D也都是真的。那么list可以存储重复的元素,但是set可以吗?

由于这两个classes之间并非is-a关系,所以public继承不适合用来塑模它们。正确的做法是,你应当了解,Set对象可根据一个list对象实现出来:

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;            //用来表述Set的数据
};

Set成员函数可大量依赖list及标准程序库其他半部分提供的机能来完成,所以其实很直观也很简单,只要你熟悉以STL编写程序:

template<typename T>
bool Set<T>::member(const T& item) const
{
    return std::find(rep.begin(), rep.end(), item)!=rep.end();
}

template<typename T>
void Set<T>::insert(const T& item)
{
    if(!member(item)) rep.push_back(item);
}

template<typename T>
void Set<T>::remove(const T& item)
{
    typename std::list<T>::iterator it = std.find(rep.begin(), rep.end(), item);
    if(it != rep.end()) rep.erase(it);
}

template<typename T>
std::size_t Set<T>::size() const
{
    return rep.size();
}

这些函数如此的简单,适合成为inling的候选人,但是别忘了查看条款30!

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

例子如下:

class Person { ... };
class Student: private Person { ... };
void eat(const Person& p);            //任何人都会吃
void study(const Student& s);         //只有学生才在学校学习

Person p;                             //p是人
Student s;                            //s是学生

eat(p);                               //没问题,p是人,会吃
eat(s);                               //错误!

【注】

  1. 如果classes之间的继承关系是private,编译器不会自动将一个派生类对象(例如Student)转换为一个基类对象(例如Person);
  2. 而且private继承中,所有继承过来的成员,在派生类中均为private属性;

Private继承的意义:

如果让D类继承B类,那么这是为了采用B类内已经备妥的某些特性,不是因为B类和D类对象存在有任何观念上的关系。private继承纯粹只是一种实现技术。借用条款34提出的术语,private继承意味只有实现部分被继承,接口部分应略去。

那么何时才使用private继承呢?

  • 当protected成员和/或virtual函数牵扯进来的时候;
  • 当一个意欲成为派生类者访问一个意欲成为基类者的protected成分;
  • 为了重新定义一或多个virtual函数;

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

需要认清的一件事是,当多重继承进入设计景框,程序又可能从一个以上的基类继承相同名称(如函数、typedef等)。那会导致较多的歧义机会。例如:

class BorrowableItem{    //图书馆允许借东西
public:
    void checkOut();     //离开时进行检查
    ...
};

class ElectronicGadget{
private:
    bool checkOut() const;    //执行自我检测
    ...
};

class MP3Player: public BorrowableItem,public ElectronicGadget
{
    ...
};

MP3Player mp;
mp.checkOut();    //产生歧义,到底是调用了哪个类中的函数?

为了解决以上带来的歧义,我们必须明确指出调用哪一个基类内的函数:

mp.BorrowableItem::checkOut();

如果调用了另外一个基类的函数,则会获得“尝试调用private成员函数”的错误!

多重继承的意思是继承一个以上的基类,但这些基类并不常在继承体系中又有更高级的基类,因为那会导致要命的“砖石型多重继承“,又称“菱形继承”,即:

class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile, public OutputFile
{ ... }

而解决这个的办法就是:虚继承!

class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile
{ ... }

使用虚继承,有以下的建议:

  1. 非必要不使用virtual bases,平常可使用non-virtual继承;
  2. 如果必要,尽可能避免在其中放置数据;

现在来看一个例子 ------ 塑模“人”的C++接口类:

class IPerson{
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
};

IPerson的客户必须以IPerson的指针和引用来编写程序,因为抽象类无法被实体化创建对象。为了创建一些可当做IPerson来使用的对象,IPerson的客户使用工厂函数将“派生自IPerson的具象classes”实体化:

//工厂函数,根据一个独一无二的数据库ID创建一个Person对象
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);

//这个函数从使用者手上获取一个数据库ID
DatabaseID askUserForDatababseID();

DatabbaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));

但是makePerson如何创建对象并返回一个指针指向它呢?无疑地一定有某些派生自IPerson的具象class,在其中makePerson可以创建对象。

假设这个class名为CPerson。CPerson必须提供“继承自IPerson”的纯虚函数的实现代码。例如,假设有个既有的数据库相关class,名为PersonInfo,提供CPerson所需要的实质东西:

class PersonInfo{
public:
    explicit PersonInfo(DatabbaseID pid);
    virtual ~PersonInfo();
    virtual const char* theName() const;
    virtual const char* theBirthDate() const;
    ...
private:
    virtual const char* valueDelimOpen() const;
    virtual const char* valueDelimClose() const;
    ...
};

PersonInfo被设计用来协助以各种格式打印数据库字段,每个字段值的起始点和结束点以特殊字符串为界。缺省的头尾界限符号是方括号(中括号),所以(例如)字段值“Ring-tailed Lemur”将被格式化为:

[ Ring-tailed Lemur ]
发布了90 篇原创文章 · 获赞 6 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_37160123/article/details/102993349