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 valuesvirtual 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 widthassert(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 mf3using Base::mf3;       // visible (and public) in Derived's scopevirtual 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 detailstypedef 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而非intstruct 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成员函数。设计结果看起来像这样:

这张图表示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 objectAddress address;                // dittoPhoneNumber voiceNumber;        // dittoPhoneNumber 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<T>继承list<T>:

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

这是错误的,因为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<T> rep;                 // representation for Set data
};
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);
}

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

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

class Person { ... };
class Student: private Person { ... };     // inheritance is now privatevoid 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;      // seevirtual 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

Effective C++: 06继承与面向对象设计相关推荐

  1. Effective C++ --6 继承与面向对象设计

    上一篇Effective C++ --5 实现 32.确定你的public继承塑模出is-a关系 (1)public 继承意味着is-a.适用于base class身上的每一件事一定也适用于deriv ...

  2. 【effective c++】继承与面向对象设计

    1.确定你的public继承塑造出is-a关系 public继承意味着is-a.适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived ...

  3. C++进阶_Effective_C++第三版(六) 继承与面向对象设计 Inheritance and Object-Oriented Design

    继承与面向对象设计 Inheritance and Object-Oriented Design 面向对象编程已经风靡编程界,关于继承.派生.virtual函数等等需要深入了解. 32.确定你的pub ...

  4. 《Effective C++》第三版 第六章 继承与面向对象设计 32~35条例

    文章目录 条款32:确定你的 `public` 继承塑膜出 is-a 关系 故事引入规则 案例说明 小结上代码 公有继承用法 企鹅不会飞 企鹅会飞,但那是错的! 总结 请记住 条款33:避免遮掩继承而 ...

  5. (6)继承与面向对象设计- Effective C++改善程序与设计的55个具体做法(Effective C++: 55 Specific Ways to Improve Your Programs)

    文章目录 32. 确定你的public继承塑模出is-a关系(Make sure public inheritance models "is-a") 33. 避免遮挡继承而来的名称 ...

  6. 《Effective C++ 3th》——继承与面向对象设计

    文章目录 Is A 确定你的public继承塑模出is-a关系 避免遮掩继承而来的名称 区分接口继承和实现继承 考虑virtual函数以外的其他选择 绝不重新定义继承而来的non-virtual函数 ...

  7. Effective C++:改善程序与设计的55个具体做法

    Effective C++:改善程序与设计的55个具体做法 二.构造/析构/赋值运算 05 Know what functions C++ silently writes and calls. 06 ...

  8. uml图中的各种箭头_设计模式学习笔记(二):UML与面向对象设计原则

    1 UML 1.1 UML UML(Unified Modeling Language)是统一建模语言,1997年11月UML1.1版本提交给OMG并正式通过,成为建模语言的个那个也标准.2003年6 ...

  9. C++面向对象设计原则详解

    概述 C++面向对象设计原则主要包括以下几点: 依赖倒置原则 开放封闭原则 单一职责原则 里氏替换原则 接口隔离原则 封装变化点原则 面向接口编程原则 优先使用对象组合,而不是类继承 接下来详细的分析 ...

最新文章

  1. 在OpenCV中基于深度学习的边缘检测
  2. 如何选择阿里云服务器配置?
  3. NOIP2012普及组 (四年后的)解题报告 -SilverN
  4. 前端开发必备!Emmet使用手册
  5. 计算机原理寻址方式ppt,计算机原理_3 寻址方式和指令系统.ppt
  6. c++求解自行车慢速比赛问题_隐马尔可夫模型(模型推断五大问题)
  7. 你的微信还安全吗?揭露清理僵尸粉的连环骗局
  8. 删除隐藏网卡(本机IP地址被占用)
  9. 网页表白代码烟花特效
  10. JPM Coin— 货币非国家化的开端 | TokenInsight
  11. Freeradius安装和配置
  12. 记英语单词中第一个易混淆的单词组attain achieve acquire
  13. Redis-NoSql结构化数据库
  14. element-ui Pagination 分页频繁切换导致重复触发api问题
  15. python3 下ascii与 str的转换
  16. 极路由B70/极路由4增强版改spi,pb-boot启动后切换回nand刷nand breed
  17. 优秀产品经理的18种能力
  18. qspi(spi四线模式)
  19. Spring-boot中使用nutz实践
  20. msvcr110.dll php,windows,_msvcr110.dll丢失,vcredist_x64.exe设置失败,windows - phpStudy

热门文章

  1. C++ 控制结构和函数(二) —— 函数I(Functions I)
  2. Linux 系统应用编程——多线程经典问题(生产者-消费者)
  3. JAVA基础进阶day01
  4. 前端学习(3092):vue+element今日头条管理-发布更新
  5. [html] 你有用过图片热区吗?它有什么运用场景?
  6. [html] 当html中使用map标签时,area中coords值如何精确定位呢?
  7. [html] html标签的属性值是否可以省略引号?为什么?
  8. 前端学习(2035)vue之电商管理系统电商系统之形成折线图
  9. 前端学习(1985)vue之电商管理系统电商系统之本地分支放到git上面保存
  10. 前端学习(1732):前端系列javascript之插入内容