effective c++之继承与面向对象设计

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

请记住:

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


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

在C++的继承体系中,不管是变量名还是函数名称的查找按照一定的规则进行:首先在局部作用域中进行查找,如果找到则停止;如果没找到则到外层进行查找,依次类推。特别需要注意的是,C++在查找的时候,只会对名字进行比较查找,与类型无关。例如:

class  Base
{
private:
    int x;
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};
 
class Derive:public  Base
{
public:
    virtual void mf1();
    void mf3();
    void mf4();
};
Derive d;
int x;
...
d.mf1();//callmf1() in Derive
d.mf1(x);//errorin Derive,the function mf1(int) in the base class if hidden
d.mf2();//ok callBase::mf2()
d.mf3();//ok call Derive::mf3()
d.mf3(x)//error in Derive,no function mf3(int),mf3(int) in Base is hidden

这些行为背后的基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带地从疏远的baseclass继承重载函数。 但是你通常会想继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就违反了base和derived之间的is-a关系,不过幸运的是,你可以使用using声明式达成目标。

#include"stdafx.h"
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();
 
       using Base::mf1;
       using Base::mf3;
};

有时候不想使用所有基类的重载函数,比如上例中只想使用无参的mf1(),可以使用转交函数(forwarding function)。

class Derived :public Base
{
public:
       virtual void mf1(){Base::mf1();}
};

Inline转交函数的另一个用途是为那些不支持using声明的老旧编译器另辟新路。

请记住:

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

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


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

表面上直截了当的公有继承的概念,实际上包含两个相互独立的部分:函数接口的继承和函数实现的继承。

一、non-virtual普通函数具体指定接口继承以及强制性实现继承

二、pure virtual纯虚函数只具体指定接口继承

声明一个pure virtual函数的目的是为了让derived class只继承函数接口。令人意外的是,竟然可以为纯虚函数提供定义,也就是为它提供一份实现代码,C++对它并没有任何怨言,但调用它的唯一途径是“调用时明确指出期class的名称”。

ps1->draw();
Sharp* ps2 =new Rectangle;
ps2->draw();
ps1->Shape::draw();     //调用Shape的draw
ps2->Shape::draw();

这样对纯虚函数的实现用途有限;但是一如稍后你将看到,它可以实现一种机制:为普通虚函数提供更平常更安全的缺省实现。

三、Impure virtual普通虚函数具体指定接口继承及缺省实现继承

声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现(普通虚函数是告诉派生类的设计者:你必须支持一个error函数,但如果你不想自己写一个,可以使用基类Shape提供的缺省版本)。

但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。例如:

class Airport{...};              //用以表示机场
class Airplane{
public:
    virtual void fly(const Airport&destination);
};
voidAirplane::fly(const Airport& destination) {
    //缺省代码,将飞机飞至指定目的地
}
class ModelA :public Airplane {...};
class ModelB :public Airplane {...};
class ModelC :public Airplane {
...//未声明fly函数,但它并不希望缺省飞行
};
AirportPDX(...);        //PDX是我家附近的机场
Airplane* pa =new ModelC;
...
pa->fly(PDX)//灾难

A型和B型两种飞机,它们以相同方式飞行,因此有了上述的继承方式。基类Airplane::fly被声明为虚函数,为了避免A和B撰写相同的飞行代码,为Airplane::fly提供了缺省的飞行方式,这被A和B继承。但是新的C型飞机的飞行方式和A、B的飞行方式不一样,但是如上代码:忘记为C定义fly函数,这将是灾难。解决方法就是将fly写成纯虚函数,这样C不得不实现fly函数。可以轻易通过“只有派生类明确说明要用基类缺省行为才为派生类提供缺省实现,否则免谈”。此间伎俩在于切断“虚函数接口”和“缺省实现”之间的连接:

class Airplane{
public:
    virtual void fly(const Airport&destination) = 0;   //注意,纯虚函数,仅提供飞行接口
    ...
protected:
    void defaultFly(const Airport&destination);    //fly的缺省行为
};
voidAirplane::defaultFly(const Airport& destination) {
    //缺省行为,将飞机飞至目的地
}
想要缺省实现的A和B可以这样做:
class ModelA:public Airplane {
public:
    virtual void fly(const Airport&destination) {  //实现缺省飞行行为
        defaultFly(destination);
    }
    ...
};
class ModelB:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        defaultFly(destination);
    }
    ...
};

这样C就不可能继承不正确的fly实现代码了,必须自己实现fly:

class ModelC:public Airplane {
public:
    virtual void fly(const Airport&destination);
    ...
};
voidModelC::fly(const Airport& destination) {
    //将C型飞机飞至指定的目的地
}

此外,有些人反对以不同的函数分别提供接口和缺省实现,向上述的fly和defaultFly那样。他们担心过度雷同的函数名称会引起类命名空间污染问题。但是他们也同意,接口和缺省实现应该分开。给纯虚函数定义:

class Airplane{
public:
    virtual void fly(const Airport& destination) = 0;
    ...
};
voidAirplane::fly(const Airport& destination) {   //纯虚函数实现
    //缺省行为,将飞机飞至指定目的地
}
class ModelA:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        Airplane::fly(destination);
    }
};
class ModelB:public Airplane {
public:
    virtual void fly(const Airport&destination) {
        Airplane::fly(destination);
    }
};
class ModelC:public Airplane {
public:
    virtual void fly(const Airport&destination)
    ...
};
voidModelC::fly(const Airport& destination) {
    // 将C型飞机飞至指定目的地
}

这个实现和前几乎一模一样,只是用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。

请记住:

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

2. pure virtual函数只具体指定接口继承;

3. impure virtual函数具体指定接口继承和缺省实现继承;

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

 

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

假设你正在写一个视频游戏软件,游戏里有各种各样的人物,每个人物都有健康状态,而且不同的人物可能以不同的方式计算他们的健康指数.该如何设计游戏里的人物,主要如何提供一个返回人物健康指数的接口。基于虚函数的方法是比较常用的:

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

替代方案一:借由Non-Virtual Interface手法实现Template Method模式(NVI)

class GameCharacter{
       public:
              int healthValue() const {         //派生类不能重新定义它
                     ...                  //做一些事前工作
                     int retVal =doHealthValue();
                     ...                  //做一些事后工作
                     return retVal;
              }
       ...
       private:
              virtual int doHealthValue() const{       //派生类可以重新定义
                     ...                  //提供缺省算法
              }
};

NVI手法的一个优点可以确保在一个virtual函数被调用之前设定好适当的场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器,制造运转日志记录项,验证class约束条件,验证函数先决条件等等。“事后工作”可以包括互斥器解除锁定,验证函数的事后条件,再次验证class约束条件等等。

替代方案二:借由Function Pointers实现Strategy模式

classGameCharacter;       //前置声明
//以下函数是计算体力值的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
       public:
              typedef int(*HealthCalcFunc)(const GameCharacter&);
              explicitGameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
              { }
              int healthValue() const{
                     return healthFunc(*this);
              }
              ...
       private:
              HealthCalcFunc healthFunc;
};

该方法优点:1、同一人物类型之不同实体可以有不同的健康计算函数,只需要在构造实例时,传入不同的计算函数的指针;2、某已知人物之健康计算函数可在运动期变更,可以在GameCharacter里提供一个成员函数setHealthCalc,用来替换当前的健康指数计算函数。缺点:该non-membernon-friend函数无法访问non-public成员。

替代方案三:借由tr1::function实现Strategy模式

class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
       public:
              //HealthCalcFunc可以是任何“可调用物”,可被调用并接受任何兼容于GameCharacter之物,返回任何兼容于int的东西,详下:
              typedef std::tr1::function<int(const GameCharacter&)> HealthCalcFunc;
              //这种定义表示HealthCalcFunc作为一种类型,接受GameCharacter类型的引用,并返回整数值,其中支持隐式类型转换
              explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf)
              {}
              int healthValue() const{
                     return healthFunc(*this);
              }
              ...
       private:
              HealthCalcFunc healthFunc;
};

不再使用函数指针,而是改用一个类型为tr1::function的对象。可以是函数指针,函数对象,或成员函数指针,只要其签名式兼容于需求端。

替代方案四:古典的Strategy模式

classGameCharacter{
       public:
              explicitGameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) :pHealthCalc(phcf)
              {}
              int healthValue() const{
                     returnpHealthCalc->calc(*this);
              }
              ...
       private:
              HealthCalcFunc* pHealthCalc;
};

请记住:

1. virtual函数的替代方案NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式;

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

3.tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与目标签名兼容”的所有可调用物。

 

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

假设class B为基类,class D为public继承自B的子类:

 class B{
   public:
       void func(){...}       
   };
class D:publicB{...}
D x;
B* pB = &x;
pB->func();          //调用B::func
D* pD = &x;
pD->func();          //调用D::func

非虚函数都是静态绑定的,通过pB调用的non-virtual函数永远是B定义的版本。回顾前一个款,我们可以知道,如果是public继承的话:

1) 适用于BaseClass的行为一定适用于DerivedClass,因为每一个DerivedClass对象都是一BaseClass对象;

2) 如果BaseClass里面有非虚函数,那么DerivedClass一定是既继承了接口,也继承了实现;

3) 子类里面的同名函数会掩盖父类的同名函数,这是由于搜索法则导致的。

如果继承类重新定义非虚函数,就会违反以上法则。

请记住:

任何情况下都不该重新定义一个继承而来的non-virtual函数。

 

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

为了简化讨论,我们知道继承的只能是两种函数:virtual和non-virtual。前条款明确说明重新定义继承而来的non-virtual函数永远是错误的,所以我们可以将讨论安全的局限于“继承一个带缺省参数值的virtual函数”。在这种情况下,本条款成立的理由就非常直接而明确了:virtual函数是动态绑定的,而缺省参数值却是静态绑定的。

class Shape{
    public:
             enum Color{RED,GREEN,BLUE};
             virtual void draw(Color color =RED)const = 0;
             ...
    };
    class Circle:public Shape{
    public:
             //改变缺省参数值
             virtual void draw(Color color =GREEN)const{ ... }
    };
    class Rectangle:public Shape{
    public:
            //当客户以对象调用此函数时,需要明确的指定参数值
           //静态绑定下不继承基类的缺省值,若以指针或引用调用则不需要指定缺省值,因为动态绑定
           //继承基类的参数缺省值
           virtual void draw(Color color)const{... }
};
   看一下下面几个指针:
   Shape* ps;
   Shape* pc = new Circle;
   Shape* pr = new Rectangle;
pc->draw(); //注意调用的是Circle::draw(RED)

首先根据其调用语句用指针这一事实,我们就知道了其调用的版本应该是该指针的动态类型的函数版本,即Circle::draw,这个问题不大。下面我们来看它的传值参数,前面我们提到缺省参数值是静态绑定的,而pc的静态类型是Shape,所以该参数的传入值是Shape的该函数版本的缺省值。为什么C++坚持以这种乖张的方式来运作呢?答案在于运行期效率,如果缺省值也是动态绑定的,那么编译期就必须要有办法在运行期为virtual函数决定适当的参数缺省值。如果这样做的话,就要比目前实现的"在编译期决定"的机制更慢而且更复杂,考虑到执行速度和实现上的简易性,C++放弃了这样的做法。为了遵循本款约定却同时提供缺省参数值给你的基类和父类,可以这样:

class Shape{
     public:
              enum Color{RED,GREEN,BLUE};
              virtual void draw(Color color =RED)const = 0;
              ...
     };
     class Circle:public Shape{
     public:
              virtual void draw(Color color =RED)const {...}
};

明显的是代码重复!何况你要是想改变缺省值的话,必须要同时改变基类和子类函数的缺省值。采用NVI(Non-VirtualInterface),可以这样:

class Shape{
     public:
              enum Color{RED,GREEN,BLUE};
              void draw(Color color = RED)const{
                     ...
                     doDraw(color);
                     ...
              }
              ...
     private:
             virtual void doDraw(Color color)const = 0; 
     };
     class Circle:public Shape{
                   ...
     private:
              virtual void doDraw(Color color){... }
     };

由于draw是non-virtual而non-virtual绝对不会被重新改写(条款36),所以color的缺省值总是为RED。

请记住:

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

 

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

看似很深奥的一句话,其实在说一件很简单的事情。public继承是“is-a“的关系,而复合有”has-a“或”根据某物实现出(is-implemented-in-terms-of)“的意思——当复合发生在应用域内的对象之间,表现出has-a关系;当它发生于实现域内则是表示“根据某物实现出”的关系。

class Address{...};
class PhoneNumber{...};
class Person{
      ...
private:
      std::string name_;
      Address address_;
      PhoneNumber voiceNumber_;
      PhoneNumber faxNumber_;
};

上述中Person class示范的就是has-a关系。很少有人对has-a和is-a感到困惑,但是比较麻烦的是is-a(是一种)和is-implemented-in-terms-of(根据某物实现出)。例如set的实现。可以采用list实现,继承是不可取的,这两个类之间并不是is-a的关系:

template<typename T>
class Set
{
public:
       bool member(const T& item)const
       {
              return find(rep.begin(),rep.end(),item) != rep.end();
       }
       void insert(const T& item)
       {
              if(!member(item))
                     rep.push_back(item);
       }
       void remove(const T& item)
       {
              typename list<T>::iteratorit = find(rep.begin(),rep.end(),item);
              if(it != rep.end())
                     rep.erase(it);
       }
       size_t size()const
       {
              return rep.size();
       }
private:
       list<T> rep;
};

总之,复合与public继承完全不同,它意味着has-a或者通过某物实现。

请记住:

1.   复合的意义和public继承完全不同。

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

 

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

先看看下面的代码:

class Person{...};
class Student:privatePerson{...};
void eat(const Person&p);
Person p;
Student s;
eat(p);//正确
eat(s);//错误

如果classes之间的继承关系是private,编译器不会自动将一个deirvedclass对象转换为一个base class对象。private继承意味着implemented-in-terms-of(根据某物实现出)。如果你让classD以private继承classB,你的用意是为了采用classB内已经备妥的某些特性,不是因为B对象和D对象存在有任何观念上的关系。private继承纯粹是一种实现技术,意味着只有实现部分被继承,接口部分应该略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,private继承在软件设计层面上没有意义,其意义只及于软件实现层面

Private和has-a有点类似,它的意义与复合(组合)的意义相同。那么问题来了,在二者之间我们如何做选择呢?答案就是尽可能使用复合,必要时才使用private继承,除了以下两种情况:

1、private继承通常比复合的级别低。当基类和派生类之间没有任何关系,但derived class需要访问protectedbase class的成员,或需要重新定义继承而来的virtual函数时,就要用private继承。例如书中例子提到的widget需要使用Timer中的虚函数onTick()。

2、private继承可以造成empty class最优化。(其实public继承也可以优化空基类,不知道为何非得阐述成private继承,另外继承空类有何意义呢?

优化空基类(EBO: empty base optimization):

//定义一个空基类
class Empty{};
class HoldsAnInt
{
private:
       int x;
       Empty e;//复合
};
//sizeof(HoldsAnInt)= 8;
classHoldsAnInt:private Empty
{
private:
       int x;
};
//sizeof(HoldsAnInt)= 4;

请记住:

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

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

 

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

慎用多重继承,因为容易造成歧义。例如:

class BorrowableItem {
public:
    void checkOut();
};
 
class ElectronicGadet {
private:
    bool checkOut() const;
};
 
class MP3Player
: public BorrowableItem  public ElectronicGadet{...}
MP3Player mp;
mp.checkOut();//调用哪个checkOut()

这与C++用来解析重载函数调用的规则相符:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找到最佳匹配函数后才检验其可取用性。本例中连最佳匹配都出现歧义,为了解决这个歧义,可以:

mp.BorrowableItem::checkOut();

你当然也可以明确调用mp.ElectronicGadget::checkOut(),但然后你会获得一个“尝试调用private成员函数”的错误。

“钻石型多重继承”,如果base classes在继承体系中又有更高级的base classes,非virtual继承会造成多份baseclass成员变量。这时要用virtual继承,你必须令所有直接继承自它的classes采用“virtual继承”。以IOFile为例:

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

这样会导致会导致类IOFile的对象中有两份File的副本。解决方法就是采用虚继承:

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

在effective c++中提到当涉及“public继承某个interfaceclass”(接口)和“private继承某个协助实现的class”(实现)的两相结合时候使用多重继承也是有必要的。

请记住:

1.   多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章