Effective C++: 06继承与面向对象设计

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

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

如果令class D以public形式继承class B,你便是告诉编译器说,每一个类型为D的对象同时也是一个类型为B的对象,但是反之不成立。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。

具体到代码上,任何函数如果期望获得一个类型为B(或pointer-to-B或reference-to-B)的实参,都也愿意接受一个D对象(或pointer-to-D或reference-to-D)。

这个论点只对pubiic继承才成立。private继承的意义与此完全不同(见条款39),至于protected继承,那是一种其意义至今仍然困惑我的东西。

 

public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子:class square应该以public形式继承class Rectangle吗?每个人都知道正方形是一种矩形,反之则不一定,这是真理,但是看下面的代码:

class Rectangle {
public:
  virtual void setHeight(int newHeight);
  virtual void setWidth(int newWidth);

  virtual int height() const;               // return current values
  virtual int width() const;
};

void makeBigger(Rectangle& r)               // function to increase r's area
{
  int oldHeight = r.height();
  r.setWidth(r.width() + 10);               // add 10 to r's width
  assert(r.height() == oldHeight);          // assert that r's
}  

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

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

class Square: public Rectangle {...};

Square s;
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());

很明显,第二个assert结果也应该永远为真。因为根据定义,正方形的宽度和其高度相同。但现在我们遇上了一个问题。我们如何调解下面各个assert判断式:调用makeBigger之前,在makeBigger函数内s的高度和宽度相同;s的宽度改变,但高度不变;makeBigger返回之后,s的高度再度和其宽度相同。

本例的根本困难是,某些可施行于矩形身上的事情却不可施行于正方形身上。但是public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。在正方形和矩形例子中,那样的主张无法保持,所以以public继承塑模它们之间的关系并不正确。

 

 

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

         1:下面的代码是一个很简单的名称遮掩的例子:

int x;

void someFunc()
{
  double x;
  std::cin >> x;
}

someFunc的x是double类型而global x是int类型,但那不要紧。C++的名称遮掩规则所做的唯一事情就是:遮掩名称。至于名称是否对应相同的类型,并不重要。本例中一个名为x的double遮掩了一个名为x的int。

 

2:导入继承之后,当派生类成员函数内引用(refer to)基类内的某物(成员函数、typedef、或成员变量)时,编译器可以找出我们所refer to的东西,因为派生类继承了声明于基类内的所有东西。实际运作方式是,派生类作用域被嵌套在基类作用域内,像这样:

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

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

此例内含一组混合了public和private名称,以及一组成员变量和成员函数名称。这些成员函数包括pure virtual,impure virtual和non-virtual三种,这是为了强调我们谈的是名称,和其他无关。这个例子也可以加入各种名称类型,例如~,nested classes和typedef。整个讨论中唯一重要的是这些东西的名称,至于这些东西是什么并不重要。

 

在Derived::mf4函数中,当编译器看到这里使用名称mf2,必须估算它refer to什么东西。编译器首先查找local作用域(也就是mf4覆盖的作用域),在那儿没找到任何东西名为mf2。于是查找其外围作用域,也就是class Derived覆盖的作用域。还是没找到任何东西名为mf2,于是再往外围移动,本例为base class。在那儿编译器找到一个名为mf2的东西了,于是停止查找。如果Base内还是没有mf2,查找动作便继续下去,首先找内含Base的那个namespace(s)的作用域(如果有的话),最后往global作用域找去。

 

再次考虑上面的例子,这次让我们重载base中的mf1和mf3,并且添加一个新版mf3到Derived去:

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();                   // fine, calls Derived::mf1
d.mf1(x);                  // error! Derived::mf1 hides Base::mf1
d.mf2();                   // fine, calls Base::mf2

d.mf3();                   // fine, calls Derived::mf3
d.mf3(x);                  // error! Derived::mf3 hides Base::mf3

如你所见,即使base classes和derived classes内的函数有不同的参数类型,而且不论函数是virtual或non-virtual,都会发生名称遮蔽。这和本条款一开始展示的道理相同,如今Derived内的函数mf3遮掩了一个名为mf3但类型不同的base函数。

 

不幸的是你通常会想继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就就违反了base和derived classes之间的is-a关系。可以使用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;       // make all things in Base named mf1 and mf3
  using Base::mf3;       // visible (and public) in Derived's scope

  virtual void mf1();
  void mf3();
  void mf4();
};

现在,继承机制将一如往昔地运作:

Derived d;
int x;

d.mf1();                 // still fine, still calls Derived::mf1
d.mf1(x);                // now okay, calls Base::mf1

d.mf2();                 // still fine, still calls Base::mf2

d.mf3();                 // fine, calls Derived::mf3
d.mf3(x);                // now okay, calls Base::mf3

有时候你并不想继承base classes的所有函数,这是可以理解的。但是在public继承下,这绝对不可能发生,因为它违反了public继承所暗示的“base和derived classes之间的is-a关系”。这也就是为什么上述using声明式被放在derived class的public区域的原因:base class内的public名称在publicly derived class内也应该是public。

然而在private继承之下它却可能是有意义的。假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。我们需要不同的技术,即一个简单的forwarding函数:

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

class Derived: private Base {
public:
  virtual void mf1()                   // forwarding function
  { Base::mf1(); }
  ...
};

Derived d;
int x;

d.mf1();                               // fine, calls Derived::mf1
d.mf1(x);                              // error! Base::mf1() is hidden

 

 

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

表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。身为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 { ... };

Shape类中声明了三个函数:第一个是纯虚函数draw,它使得Shape成为了一个抽象类,所以客户不能够创建Shape类的实体,只能创建其derived classes的实体,而且derived classes中必须实现自己的draw函数(否则会报编译错误);第二个是虚函数error;第三个是普通函数objectID;

 1:声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

Shape::draw函数是个纯虚函数,因为所有Shape对象都应该是可绘出的,但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法迥异于矩形绘法。Shape::draw的声明乃是对具象derived classes设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”

在C++中,可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码。

 

2:声明(非纯)虚函数的目的,是让derived classes继承该函数的接口和缺省实现。

Shape::error函数是个虚函数,它表示每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉derived classes的设计者,“你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本”。

 

3:声明普通非虚函数的目的是为了令derived classes继承函数的接口及一份强制实现。

如果成员函数是个非虚函数,意味是它并不打算在derived classes中有不同的行为。实际上一个非虚成员函数所表现的不变性(invariant)凌驾其特异性(specialization ),因为它表示不论derived class变得多么特异化,它的行为都不可以改变,所以它绝不该在derived class中被重新定义。

Shape::objectID函数是个非虚函数,它的声明表示:“每个Shape对象都有一个用来产生对象识别码的函数;此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。

 

 

35:考虑virtual函数以外的选择

         下面的代码中,GameCharacter表示游戏中的人物角色,成员函数healthValue表示人物的健康程度:

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

         由于不同的人物可能以不同的方式计算他们的健康指数,因此将healthValue声明为virtual似乎是再明白不过的做法,该函数并未被声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法。

 

         下面是几种不使用virtual的替代方法:

         1:由Non-Virtual interface手法实现Template Method模式

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

class GameCharacter 
{
public:
  int healthValue() const 
  { 
    printf("begin of healthValue\n");
    int retVal = doHealthValue();  
    printf("end of healthValue\n");
    return retVal;
  }
private:
  virtual int doHealthValue() const 
  {
    printf("this is GameCharacter::doHealthValue\n");
  }   
};

class GCA : public GameCharacter
{
private:
    virtual int doHealthValue() const 
    {
        printf("this is GCA::doHealthValue\n");
    }
};

int main()
{
    GameCharacter gc;
    GCA gca;

    gc.healthValue();
    gca.healthValue();

    GameCharacter *pgc1 = new GameCharacter;
    GameCharacter *pgc2 = new GCA;

    pgc1->healthValue();
    pgc2->healthValue();

    delete pgc1;
    delete pgc2;
}

 这种方法,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式(与C++ templates并无关联)的一个独特表现形式。

NVI手法的一个优点隐身在“做一些事前工作”和“做一些事后工作”之中。也就是确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。上述代码的结果如下:

begin of healthValue
this is GameCharacter::doHealthValue
end of healthValue
begin of healthValue
this is GCA::doHealthValue
end of healthValue
begin of healthValue
this is GameCharacter::doHealthValue
end of healthValue
begin of healthValue
this is GCA::doHealthValue
end of healthValue

 

 2:由Function Pointers实现Strategy模式

另一个更戏剧性的设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:

class GameCharacter; // forward declaration

// function for the default health calculation algorithm
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可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。

 

3:由std::function完成Strategy模式

基于函数指针的做法有些苛刻而死板:为什么要求“健康指数之计算”必须是个函数,而不能是某种“像函数的东西”呢?如果一定得是函数,为什么不能够是个成员函数?为什么一定得返回int而不是任何可被转换为int的类型呢?

可以改用一个类型为std::function的对象,这些约束就全都不见了。这样的对象可持有任何可调用物,比如函数指针、函数对象、或成员函数指针等:

class GameCharacter; 
int defaultHealthCalc(const GameCharacter& gc); 

class GameCharacter {
public:
   // HealthCalcFunc is any callable entity that can be called with
   // anything compatible with a GameCharacter and that returns anything
   // compatible with an int; see below for details
   typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
   
   explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
   : healthFunc(hcf)
   {}

   int healthValue() const
   { return healthFunc(*this);   }

private:
  HealthCalcFunc healthFunc;
};

         HealthCalcFunc是一种std::function类型,这种类型的对象可以持有任何与其签名函数兼容的可调用物。其签名函数“接受一个reference指向const GameCharacter,并返回int"。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int:

short calcHealth(const GameCharacter&); //返回short而非int

struct HealthCalculator {                  //函数对象
  int operator()(const GameCharacter&) const 
  { ... } 
};

class GameLevel {
public:
  float health(const GameCharacter&) const;  //成员函数,返回float
  ... 
}; 

class EvilBadGuy: public GameCharacter { 
  ...
};
class EyeCandyCharacter: public GameCharacter {  
  ...  
}; 

EvilBadGuy ebg1(calcHealth);   //使用函数

EyeCandyCharacter ecc1(HealthCalculator());    //使用函数对象

GameLevel currentLevel;           //使用成员函数
EvilBadGuy ebg2(
  std::bind(&GameLevel::health, currentLevel, _1)
);

 

          4:传统的Strategy模式

         传统的Strategy做法会将健康计算函数做成一个分离的继承体系中的virtual成员函数。设计结果看起来像这样:

 Effective C++: 06继承与面向对象设计_第1张图片

         这张图表示GameCharacter是某个继承体系的根类,体系中的EvilBadGuy和EyeCandyCharacter都是derived classes;HealthCalcFunc是另一个继承体系的根类,体系中的S1owHealthLoser和FastHealthLoser都是derived classes,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcF}nc继承体系的对象。具体的代码如下:

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函数

之前的条款说过,所谓public继承意味is-a的关系;在class内声明一个non-virtual函数会为该class建立起一个不变性,凌驾其特异性。如果将这两个观点施行于两个classes:B(ase)和D(erived)以及non-virtual成员函数B::mf身上,意味着:适用于B对象的每一件事,也适用于D对象;B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数。

如果D重新定义mf,这样便出现矛盾:如果D真有必要实现出与B不同的mf,那么“每个D都是一个B”就不为真。既然如此D就不该以public形式继承B。另一方面,如果D真的必须以public方式继承B,并且如果D真有需要实现出与B不同的mf,那么mf就无法为B反映出“不变性凌驾特异性”的性质。既然这样mf应该声明为virtual函数。最后,如果每个D真的是一个B,并且如果mf真的为B反映出“不变性凌驾特异性”的性质,那么D便不需要重新定义mf,而且它也不应该尝试这样做。

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

 

 

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

virtual函数系动态绑定,而缺省参数值却是静态绑定。

为什么C++坚持以这种乖张的方式来运作呢?答案在于运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢而且更复杂。为了程序的执行速度和编译器实现上的简易度,C++做了这样的取舍。

 

 

38:通过复合塑模出has-a或“根据某物实现出”(is-implemented-in-terms-of)

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

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

class Person {
public:
  ...
private:
  std::string name;               // composed object
  Address address;                // ditto
  PhoneNumber voiceNumber;        // ditto
  PhoneNumber faxNumber;          // ditto
};

上面的代码中,Person对象由string,Address,PhoneNumber构成。

 1:复合有两个意义:has-a(有一个),或这是is-implemented-in-terms-of(根据某物实现出)。

因为你正打算在你的软件中处理两个不同的领域。如果程序中的对象相当于世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域部分。其他对象则纯粹是实现细节上的人工制品,像是缓冲区、互斥锁、查找树等等。这些对象相当于软件的实现域。

当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。

 

2:has-a的关系很好区分,比较麻烦的是区分is-a和is-implemented-in-terms-of这两种对象关系。

比如:某些情况下必须自己实现一个sets而不能使用标准库提供的版本。实现sets的方法很多,其中一种便是在底层采用标准库的linked lists。

首先想到让set继承list

template                       // the wrong way to use list for Set
class Set: public std::list { ... };

这是错误的,因为public继承意味着is-a的关系,如果D是一种B,对B为真的每一件事情对D也都应该为真。但list可以内含重复元素,但是set的定义却不允许包含重复元素。因此“Set是一种list”并不为真。

正确的做法是,Set对象可根据一个list对象实现出来:

template<class T>                   // the right way to use list for Set
class Set {
public:
bool member(const T& item) const;
  void insert(const T& item);
  ...
private:
  std::list rep;                 // representation for Set data
};
template
bool Set::member(const T& item) const
{
  return std::find(rep.begin(), rep.end(), item) != rep.end();
}

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

 

 

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

         1:如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象:

class Person { ... };
class Student: private Person { ... };     // inheritance is now private

void eat(const Person& p);                 // anyone can eat

Person p;                                  // p is a Person
Student s;                                 // s is a Student
eat(s);                                    // error! a Student isn't a Person

         上面针对s的eat调用将会报错,当eat的形参是Person或Person*时也一样,都会报错:error: ‘Person’ is an inaccessible base of ‘Student’。

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

 

          2:Private继承意味着implemented-in-terms-of(根据某物实现出)。如果让class D以private形式继承class B,你的用意是为了采用class B内已经具备的某些特性,不是因为B对象和D对象存在有任何观念上的关系。因此,private继承纯粹只是一种实现技术,如果D以private形式继承B,意思是D对象根据B对象实现而得,再没有其他意涵了。

 

         3:Private继承意味is-implemented-in-terms-of,之前的条款指出复合的意义也是这样。如何在两者之间取舍?答案很简单:尽可能使用复合,必要时才使用private继承。何时才是必要?主要是当protected成员和/或virtual函数牵扯进来的时候。

        

         4:为了能够知道Widget成员函数的调用频率,需要记录每个成员函数的调用次数,然后周期性的审查这些信息。为了完整这个工作,需要设定一个定时器,周期性的取出Widget的状态。

         假设当前有一个定时器类:

class Timer {
public:
  virtual void onTick() const;          // automatically called for each tick
  ...
};

         onTick函数会周期性的执行。因此,可以重新定义那个onTick函数,让其取出Widget当前状态。

为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但public继承在此例并不适当,因为Widget显然并不是个Timer。这种情况下,必须以private形式继承Timer:

class Widget: private Timer {
private:
  virtual void onTick() const;           // 查看Widget的数据等等..
  ...
};

        藉由private继承,Timer的public OnTick在Widget内变成private了。

 

5:上面的方法不是唯一实现目的的方法,其实可以使用复合:

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

 使用复合要比使用private继承有更多的优势:首先,你或许会想设计Widget使它得以拥有derived classes,但同时你可能会想阻止derived classes重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived classes将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。

 

6:private继承主要用于“当一个意欲成为derived class者想访问base class的protected成分,或为了重新定义一或多个virtual函数”,但这时候两个classes之间的概念关系其实是is-implemented-in-terms-of而非is-a。

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

 

7:private继承还适用于一种比较激进的情况:如果一个类不带任何数据,也就是它没有non-static成员变量,没有virtual函数(因为这种函数会为每个对象带来一个vptr),也没有virtual base classes(这样的base classes也会导致体积的额外开销)。这种类的对象不使用任何空间,因为没有隶属于对象的数据需要存储。但是C++规定,凡是独立(非附属)的对象必须有非0大小,所以:

class Empty 
{
public:
    fun() {printf("this is fun");}
private:
    fun2() {printf("this is fun2");}
};                      

class HoldsAnInt {  
private:
  int x;
  Empty e;   
};

  上面的类定义,sizeof(HoldsAnInt)会大于sizeof(int),测试结果是sizeof(Empty)为1, sizeof(int)为4,而sizeof(HoldsAnInt)为8。因为面对大小为0的独立非附属对象,C++要求默默插入一个char到空对象中,然而因为内存对其的需求,所以sizeof(HoldsAnInt)为8。

 上面的情况适用于独立非附属对象,但是不适用于derived class内的base class成分,因为它不是独立非附属的,因此:

class HoldsAnInt: private Empty
{
private:
    int x;
};

这样的定义,sizeof(HoldsAnInt)等于sizeof(int),这就是所谓的EBO(empty base optimization;空白基类最优化),EBO一般只在单一继承可行。

注意,上面的空类不是真的empty,它可以包含typedefs, enums, static成员变量或non-virtual函数。

 

 

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

 1:多重继承情况下,派生类可能从多个base class继承相同的名称,从而导致歧义:

class BorrowableItem {  
public:
  void checkOut();   
};

class ElectronicGadget {
private:
  bool checkOut(int a) const;  
};

class MP3Player:                 
  public BorrowableItem,         
  public ElectronicGadget
{ };                       

MP3Player mp;
mp.checkOut();                     // ambiguous! which checkOut?

 上面的代码对checkOut的调用会报错:” reference to ‘checkOut’ is ambiguous”,及时两个候选函数的访问权限不同,参数也不相同。为了解决歧义,必须明确指出要调用哪一个base class内的函数:mp.BorrowableItem::checkOut()

 

         2:多重继承的情况下,有可能形成“钻石型多重继承”的情况。为了避免某个数据发生多份拷贝的情况,必须使那些带有此数据的class成为一个virtual base class:

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

         这种方法的缺点是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢。种种细节因编译器不同而异,但基本重点很清楚:你得为virtual继承付出代价。

另外,virtual base的初始化责任是由继承体系中的最低层(most derived) class负责,这表示:(1)classes若派生自virtual bases而需要初始化,必须认知其virtual bases--不论那些bases距离多远;(2)当一个新的derived class加入继承体系中,它必须承担其virtual bases(不论直接或间接)的初始化责任。

我对virtual base classes(亦相当于对virtual继承)的忠告很简单。第一,非必要不使用virtual bases。平常请使用non-virtual继承。第二,如果你必须使用virtual base classes,尽可能避免在其中放置数据。这么一来你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。

 

下面是一种多重继承的合理应用场景:

class IPerson {
public:
  virtual ~IPerson();

  virtual std::string name() const = 0;
  virtual std::string birthDate() const = 0;
};

IPerson是个Interface class,CPerson是要继承该类并需要提供继承自IPerson的pure virtual函数的实现代码。现在有个现成的类PersonInfo,它可以完成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;      // see
  virtual const char * valueDelimClose() const;     // below
};

PersonInfo用于以各种格式打印数据库字段,每个字段值的起始字符和终止字符由valueDelimOpen和valueDelimClose返回,默认的实现分别是’[’ 和 ’]’,但是这两个界限符号并非人人喜欢,因此valueDelimOpen和valueDelimClose是virtual函数,允许派生类设置自己的界限符号。所以,PersonInfo::theName的实现可能如下:

const char * PersonInfo::valueDelimOpen() const
{
  return "[";                       // default opening delimiter
}

const char * PersonInfo::valueDelimClose() const
{
  return "]";                       // default closing delimiter
}

const char * PersonInfo::theName() const
{
  static char value[Max_Formatted_Field_Value_Length];

  // write opening delimiter
  std::strcpy(value, valueDelimOpen());

  //append to the string in value this object's   name field (being careful
  //to avoid buffer overruns!)

  // write closing delimiter
  std::strcat(value, valueDelimClose());

  return value;
}

作为CPerson的实现者,发现可以使用PersonInfo实现name和birthDate,但是需要界限符号为空。因此,CPerson和PersonInfo的关系是is-implemented-in-terms-of,我们知道这种关系可以有两种技术实现:复合和private继承。条款39指出复合通常是较受欢迎的做法,但如果需要重新定义virtual函数,那么继承是必要的。本例之中CPerson需要重新定义valueDelimOpen和valueDelimClose,所以单纯的复合无法应付。最直接的解法就是令CPerson以private形式继承PersonInfo。

CPerson也必须实现IPerson接口,因此需要以public继承IPerson。因此这就是多重继承的一个通情达理的应用:

class CPerson: public IPerson, private PersonInfo {     // note use of MI
public:
  explicit CPerson(    DatabaseID pid): PersonInfo(pid) {}
  virtual std::string name() const                     
  { return PersonInfo::theName(); }                    
                                               
  virtual std::string birthDate() const              
  { return PersonInfo::theBirthDate(); }
private:                                               
  const char * valueDelimOpen() const { return ""; }    
  const char * valueDelimClose() const { return ""; }  
};  

最后,需要注意的是,如果某种需求下,你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想--几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。只要确定,你的确是在明智而审慎的情况下使用它。

 

转载于:https://www.cnblogs.com/gqtcgq/p/7849533.html

你可能感兴趣的:(c/c++,设计模式,数据库)