继承与面向对象设计

来源:互联网 发布:ubuntu查看opencv路径 编辑:程序博客网 时间:2024/05/16 05:53

继承与面向对象设计

C++的OOP有可能和你原本习惯的OOP稍有不同:

  1. “继承”可以是单一继承或多重继承,

  2. 每一个继承连接(link)可以是public, protected或private.

  3. 也可以是virtual或non-virtual。然后是成员函数的各个选项:virtual? non-virtual?pure virtual?

  4. 以及成员函数和其他语言特性的交互影响?

  5. 缺省参数值与virtual函数有什么交互影响?

  6. 继承如何影响C++的名称查找规则?设计选项有哪些?

  7. 如果class的行为需要修改,virtual函数是最佳选择吗?

本章对这些题目全面宣战。此外我也解释C++各种不同特性的真正意义,也就是当你使用某个特定构件你真正想要表达的意思。

  1. “public继承”意味’'is-a",如果你尝试让它带着其他意义,你会惹祸上身。

  2. virtual函数意味“接口必须被继承”.

  3. non-virtual函数意味“接口和实现都必须被继承”。

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

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

对象可派上用场的任何地方,D对象一样可以派上用场” 最重要。

public继承和is a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子,企鹅( penguin )是一种鸟,这是事实。鸟可以飞,这也是事实。如果我们天真地以C++描述这层关系,结果如下:

class Bird{public:virtual void fly();//鸟可以飞class Penguin: public Bird{//企鹅是一种鸟  ...};

应该意识到:有数种鸟不会飞。

class Bird{...};//没有声明fly函数class FlyingBird: public Bird{public:    virtual void fly();}class Penguin: public Bird{  //没有声明fly函数}

另一种办法是让Base类。拥有fly接口。

void error(const std::string& msg);//定义于另外某处class Penguin: public Bird{public:    virtual void fly(){       error("Attempt to make a penguin fly!”);     }};

在运行的时候报错。

class Bird{  //没有声明fly函数};class Penguin: public Bird{  //没有声明fly函数};

在编译期报错,比留在运行期报错要靠谱得多。

类似的例子还有Rectangle和Square。

如果你令class D ( "Derived")以public形式继承class B ( "Base" ),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。

你的意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”(译注:此即所谓Liskov Substitution Principle),因为每一个D对象都是一种(是一个)B对象。反之如果你需要一个D对象,B对象无法效劳,因为虽然每个D对象都是一个B对象,反之并不成立。

is一并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。 private这些关系将在条款38和39讨论。将上述这些重要的相互关系中的任何一个误塑为is-a而造成的错误设计,在C一中并不罕见,所以你应该确定你确实了解这些个“classes相互关系”之间的差异,并知道如何在C++-中最好地塑造它们。

  • "public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

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

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

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

Rule 34 区分接口继承和实现继承

对于public继承,实际上分为了:

  1. 函数接口(function interfaces)继承.

  2. 函数实现(function implementations)继承.

身为class设计者,有时候你会希望

  • derived classes只继承成员函数的接口(也就是声明);

  • 有时候你又会希望derived classes同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;

  • 又有时候你希望derived classes同时继承函数的接口和实现,并且不允许覆写任何东西。

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

其中draw函数为纯虚函数(pure-virtual),所以Shape不能够实例化。

error为虚函数impure-virtual。objectID为non-virtual.

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。(它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。( 但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。

在impure virtual函数中实现其中的缺省函数容易造成未明白说出“我要”的情况下就继承了该缺省行为。

class Airport{...};class Airplanepublic://用以表现机场virtual void fly(const Airport& destination);void Airplane::fly(const Airport& destination){//缺省代码,将飞机飞至指定的目的地}class ModelA: public Airplane{...};class ModelB: public Airplane{...};

在这里如果新产生ModelC 忘记了实现fly,可能就会使用到缺省版本,这样不安全。使用defaultfly和fly分离的方法。

class Airplane{public:virtual void fly(const Airport& destination)=0;protected:    void defaultFly(const Airport& destination);};void Airplane::defaultFly(const Airport& destination){    //缺省行为,将飞机飞至指定的目的地。}class ModelA: public Airplane{public:    virtual void fly(const Airport& destination)    {defaultFly(destination);}};

有些人反对以不同的函数分别提供接口和缺省实现,像上述的fly和defaultFly那样。他们关心因过度雷同的函数名称而引起的class命名空间污染问题。但是他们也同意,接口和缺省实现应该分开。

我们可以利用“pure virtual函数必须在derived classes中重新声明,但它们也可以拥有自己的实现”这一事实。下面便是Airplane继承体系如何给pure virtual函数一份定义:

class Airplane{public:    virtual void fly(const Airport& destination) = 0;  //  ...    };    void Airplane::fly(const Airport& destination)  //pure virtual函数实现{//缺省行为,将飞机飞至指定的目的地}class ModelA: public Airplane{public:    virtual void fly(const Airport& destination)//必须重新声明 只是这里的实现采用了Base的实现    {Airplane::fly(destination);}}class Mode1B: public Airplane{public:    virtual void fly(const Airport& destination)    {Airplane::fly(destination);}}
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

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

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

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

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

Rule 35 考虑virtual函数以外的其他选择

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

class GameCharacter{public:    virtual int healthValue()const;//返回人物的健康指数;//derived classes可重新定义它。};

藉由Non-Virtual Interface手法实现Template Method模式

我们将从一个有趣的思想流派开始,这个流派主张virtual函数应该几乎总是private。这个流派的拥护者建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:

class GameCharacter{public://derived classes不重新定义它,    int healthValue()const{//见条款36      //...//做一些事前工作,详下。        retVal=doAealthValue();//做真正的工作。      //做一些事后工作,详下。      return retValue;    }private:    virtual int doHealthValue()const     //derived classes可重新定义它。};

这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface (NVI)手法。它是所谓Template Method。

藉由Function Pointers实现Strategy模式

class GameCharacter;//前置声明(forward declararion)//以下函数是计算健康指数的缺省算法。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函数”的做法比较,它提供了某些有趣弹性.

但是需要注意是否需要访问到non-public部分,以及该函数是否需要声明为friend的问题。

藉由tr1::function完成Strategy模式

该Strategy脱离了必须是函数的局限性。

如果我们不再使用函数指针(如前例的healthFunc),而是改用一个类型为trl::function的对象,这些约束就全都挥发不见了。就像条款54所说,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象、或成员函数指针),只要其签名式兼容于需求端。以下将刚才的设计改为使用

trl::function:class GameCharacter;int defaultHealthCalc(const GameCharacter& gc);class GameCharacter{public://如前//如前//HealthCalcFunc可以是任何“可调用物”( callable entity ),可被调用并接受//任何兼容于GameCharacter之物,typedef std::trl::function<int (const GameCharacter&)>HealthCalcFunc;//返回任何兼容于int的东西。详下。explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)  :healthFunc (hcf){}int healthValue()const{return healthFunc(*this);}private:  HealthCalcFunc healthFunc;};

其中最bug的地方就是可以接受隐式类型转换。也就是所返回为double类型的函数在function obj中也是可以被适配的。

古典的Strategy模式

如果你对设计模式(design patterns)比对C++的酷劲更有兴趣,我告诉你,传统(典型)的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:

class GameCharacter;class HealthCalcFunc{public://前置声明(forward declararion )virtual int calc(const GameCharacter& gc) const{//…}};HealthCalcFunc defaultHealthCalc;//GameCharacter 包含了HealthCalcFunc class GameCharacter{public:    explicit GameCharacter(HealthCalcFunc* phcf=&defaultHealthCalc)      :pHealthCalc(phcf)      {}    int healthValue()const    {return pHealthCalc->calc(*this);}private:    HealthCalcFunc* pHealthCalc;};
  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。

  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。

  • trl::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

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

class B{public:    void mf(); };//不覆盖版本class D: public B{};/*这种情况下没有歧义*/class D:public B{  public:  void mf();}D x;D* pD = &x;B* pB = &x;//遮掩(hides)了B::mf;见条款33pB->mf();//调用B::mfpD->mf();//调用D::mf

出现以上分歧的原因就在于not-virtual函数是静态绑定。意味着pB被声明为pointer-to-B,调用的一定是B所定义的版本。

virtual函数是动态绑定,如果需要该特性就不应该声明为non-virtual.和non-virtual继承接口和实现的定义不同。

  • 绝对不要重新定义继承而来的non-virtual函数。

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

首先函数分为virtual和non-virtual函数。Rule36已经表明了non-virtual函数不应该改变。那么现在就讨论virtual函数部分。问题变为:“继承一个带有缺省参数值的virtual函数”

这种情况下,本条款成立的理由就非常直接而明确了:virtual函数系动态绑定( dynamically bound ),而缺省参数值却是静态绑定(statically bound )

动态绑定:在运行时决定调用。

静态绑定:在编译时决定。

//一个用以描述几何形状的classclass 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) const;    //译注:请注意,以上这么写则当客户以对象调用此函数,一定要指定参数值。    //因为静态绑定下这个函数并不从其base继承缺省参数值。    //但若以指针(或reference)调用此函数,可以不指定参数值,    //因为动态绑定下这个函数会从其base继承缺省参数值。};

Shape* ps;//静态类型为Shape*Shape* pc=new Circle;//静态类型为Shape*Shape* pr=new Rectangle;//静态类型为Shape*

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

ps=pc;//的动态类型如今是Circle*ps=pr;//的动态类型如今是Rectangle*pc->draw(Shape::Red);//调用Circle::draw(Shape::Red)pr->draw(Shape::Red);//调用Rectangle::draw(Shape::Red)pr->draw();//调用Rectangle::draw(Shape::Red)!

此例之中,pr的动态类型是Rectangle,所以调用的是Rectangle的virtual函数,一如你所预期。Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是Shape,所以此一调用的缺省参数值来自Shape class而非Rectangle class !结局是这个函数调用有着奇怪并且几乎绝对没人预料得到的组合,由Shape class和Rectangle class的draw声明式各出一半力。

C++为了效率,选择在编译期决定了函数的参数而不是在运行期。

  • 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数—你唯一应该覆写的东西—却是动态绑定。

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

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

class Address{};//某人的住址class PhoneNumber{};class Person{public:private:    std::string name;//合成成分物(composed object)    Address address;    PhoneNumber voiceNumber;    PhoneNumber faxNumber;};

复合意味has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。

  • 应用域:程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域(application domain)部分。当复合发生于应用域内的对象之间,表现出has a的关系.

  • 实现域:其他对象则纯粹是实现细节上的人工制品,像是缓冲区(buffers )、互斥器(mutexes),查找树(search trees)等等。这些对象相当于你的软件的实现域(implementation domain )。实现域内则是表现is-implemented-in-terms-of的关系。

  • 复合(composition )的意义和public继承完全不同。

  • 在应用域(application domain ),复合意味has-a(有一个)。在实现域(implementation domain ),复合意味is-implemented-in-terms-of(根据某物实现出)。

Rule 39 明智而审慎地使用private继承

class Person{};class Student:private Person{};//这次改用private继承void eat(const Person& p);//任何人都会吃void study(const Student& s);//只有学生才在校学习Person p;//p是人Student s;//s是学生eat (p);//没问题,p是人,会吃。eat (s);//错误!吓,难道学生不是人?!

显然private继承并不意味IS一关系。那么它意味什么?

  1. 编译器不会自动将一个derived class对象(例如Student)转换为一个base class对象(例如Person)。

  2. 由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。

Private继承意味implemented-in-terms-of(根据某物实现出)。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。

Private继承意味is-implemented-in-terms-of(根据某物实现出),这个事实有点令人不安,因为条款38才刚指出复合(composition )的意义也是这样。你如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。其实还有一种激进情况,那是当空间方面的利害关系足以踢翻private继承的支柱时。稍后我们再来操这个心,毕竟它只是一种激进情况。

我们决定修改Widget Class,让它记录每个成员函数的被调用次数。运行期间我们将周期性地审查那份信息,也许再加上每个Widget的值,以及我们需要评估的任何其他数据。为完成这项工作,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了。

class Timer{public:    explicit Timer(int tickFrequency);    virtual void onTick()const;//定时器每滴答一次,//此函数就被自动调用一次。};//此时Widget 需要继承Timer 但是Widget和timer之间的关系不是is a 不能用public继承class Widget:private Timer{  private:  virtual void onTick() const;};

这个设计是可以达到目的的,但是不是最佳。应该优先使用复合。

class Widget{private:class WidgetTimer:public Timer{public:    virtual void onTick()const;};Widget Timer timer;};

比较以上两周设计,在Widget直接private继承Timer中。在Widget子类也必须实现Timer接口,导致Timer的OnTick可能被修改。而第二个方案没有这种风险。

如果考虑到编译依存性最低,应该把Timer从Widget中移出,然后在Widget保留一个指向实体Timer的指针。

private 还有一种处于减少空间浪费的情况。那是在空对象时,由于C++编译器默认会生成一个字节大小。为了消除可以使用private继承。 几乎可以确定sizeof(HoldsAnInt)==sizeof(int)。这是所谓的EBO (emptybase optimization;空白基类最优化),我试过的所有编译器都有这样的结果。如果你是一个程序库开发人员,而你的客户非常在意空间,那么值得注意EBO。

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

  • Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。

  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

Rule 40 明智而审慎地使用多重继承

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

class BorrowableItem//图书馆允许你借某些东西public:      void checkout();//离开时进行检查};class ElectronicGadget{private:      bool checkout() const;//执行自我检测,返回是否测试成功};class MP3Player:    public BorrowableItem,    public ElectronicGadget{};//注意这里的多重继承//(某些图书馆愿意借出MP3播放器)//这里,class的定义不是我才门的关心重点MP3Player mp;mp.checkout();//歧义!调用的是哪个checkout?mp.Borrowableltem::checkOut();//哎呀,原来是这个checkout...

这与C++用来解析(resolving)重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配函数后才检验其可取用性。

  • 多重继承的意思是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致要命的“钻石型多重继承”:

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

问题来了:File中的成员变量在IOFile中应该持有一份还是两份呢?

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

这样声明就会保证子类只有一份成员变量。但是这样声明效率会非常低,不建议这样使用。

下面这个例子为需要利用多重继承的例子:

现在让我们看看下面这个用来塑模“人”的C++ Interface class(见条款31):

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

声明为纯虚函数,抽象类,必须通过具类来实现。假设为Cperson。

此时还有一个类,和数据库相关,提供了Cperson需要的信息。

class PersonInfo{public:    explicit PersonInfo(DatabaseID pid);    virtual ~PersonInfo();    virtual const char* theName()const;    virtual const char* theBirthDate()const;private:    virtual const char* valueDelimOpen()const;    virtual const char* valueDelimClose()const;//详下}

这个时候Cperson is Persion,所以需要public继承IPersion。去实现它相关的接口。而对于PersonInfo,Cperson需要用到它的实现。根据原则可以使private继承或者是复合来实现。但是其中需要修改PersonInfo中的接口,所以使用private继承合理.

class CPerson: public IPerson, private PersonInfo{//注意,多重继承public:    explicit CPerson(DatabaseID pid):PersonInfo(pid){}    virtual std::string name()const//实现必要的IPerson成员函数    {return PersonInfo::theName();}    virtual std::string birthDate() const;//实现必要的工Person成员函数    {return PersonInfo::theBirthDate();}private://重新定义//继承而来的    const char* valueDelimOpen()const{return "";}    const char* valueDelimClose()const{return "";}  //virtual//”界限函数”
  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。

  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。

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


原创粉丝点击