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() + ); // 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() = ;
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() = ;
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() = ;
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() = ;
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 = ;
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成员函数。设计结果看起来像这样:
这张图表示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<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 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 = ;
virtual std::string birthDate() const = ;
};
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 ""; }
};
最后,需要注意的是,如果某种需求下,你唯一能够提出的设计方案涉及多重继承,你应该更努力想一想--几乎可以说一定会有某些方案让单一继承行得通。然而多重继承有时候的确是完成任务之最简洁、最易维护、最合理的做法,果真如此就别害怕使用它。只要确定,你的确是在明智而审慎的情况下使用它。
Effective C++: 06继承与面向对象设计的更多相关文章
- Effective C++(20) 继承与面向对象设计
本文主要参考<Effective C++ 3rd>中的第六章部分章节的内容. 关注的问题集中在继承.派生.virtual函数等.如: virtual? non-virtual? pure ...
- Effective C++ 6.继承与面向对象设计
//条款32:确定你的public继承塑模出is-a关系 // 1.public继承意味着is-a的关系,适用在基类上的方法都能用于派生类上. //条款33:避免遮掩继承而来的名称 // 1.在pub ...
- 【Effective C++】继承与面向对象设计
关于OOP 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private,也可以是virtual或non-virtual. 2,成员函数的各个选项:virtu ...
- EffectiveC++ 第6章 继承与面向对象设计
我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的"可能比较准确"的「翻译」. Chapter 6 继承与面向对象设计 Inheritance and ...
- Effective C++ —— 继承与面向对象设计(六)
条款32 : 确定你的public继承塑模出is-a关系 以C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味“is-a”(是一种)的关系.请务必牢记.当 ...
- Effective C++ -- 继承和面向对象设计
32.确保你的public继承了模is-a关系 public继承意味着is-a关系(里氏替换原则),一切适用于基类也适用于派生类. 矩形继承正方形问题: 可实施与矩形的操作无法实施与正方形 在编程领域 ...
- Effective C++ ——继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系 以public继承的类,其父类的所有的性质都应该使用与子类,任何需要父类的地方都应该能用子类来代替,任何子类类型的对象也同时是父类的: class ...
- Effective C++笔记:继承与面向对象设计
关于OOP 博客地址:http://www.cnblogs.com/ronny 转载请注明出处! 1,继承可以是单一继承或多重继承,每一个继承连接可以是public.protected或private ...
- Effective C++笔记(六):继承与面向对象设计
参考:http://www.cnblogs.com/ronny/p/3756494.html 条款32:确定你的public继承塑模出is-a关系 “public继承”意味着is-a.适用于base ...
随机推荐
- response - 文件下载
## 案例: * 文件下载需求: 1. 页面显示超链接 2. 点击超链接后弹出下载提示框 3. 完成图片文件下载 * 分析: 1 ...
- PAT甲级——A1059 Prime Factors
Given any positive integer N, you are supposed to find all of its prime factors, and write them in t ...
- [code]图像亮度调整enhancement
//draft 2013.9 //F=X2/u; ////远处细节被淹没. 亮的地方增亮明显,暗的地方更暗. 不可取. // CvScalar rgb; // rgb=cvAvg(src); //fo ...
- 在云计算环境中使用Hadoop
- groups 用户所归属的用户组查询
groups 用法很简单,就是查询用户所归属哪个或哪些用户组: 语法格式: groups 用户名 实例: [beinan@localhost ~]$ groups beinan 注:查询bein ...
- 部分树形DP的优化
ural1018. Binary Apple Tree 题目大意 有一棵n个节点的树,树上每个节点有一个值,选择m个节点使这些节点值的和最大 要求:如果选当前节点,则必须选它的父节点 解法: 我们设d ...
- []==![] 为什么等于true?
最近碰到这样一个问题: []==![] 为什么等于true? 首先分析 !的优先级较==高,先运算==两侧的操作数: typeof []; //"object" typeof ...
- NFS挂载服务具体的实施方案
1.服务器磁盘共享实施方案 第一步:安装NFS和rpc. 1. 安装nfs-utils:NFS主程序,rpcbind:PRC主程序 nfs-utils:NFS主程序,包含rpc.nfsd rpc.m ...
- JAVA数据库连接池的革命 -- 从BoneCP到HikariCP(转)
从BoneCP到HikariCP 今天笔者本想更新一下项目中使用到的BoneCP版本的.却无意发现jolbox网站打不开了.起初以为是被墙掉了,经过一番查找,居然在BoneCP的Github站看到了如 ...
- 通过sql 向数据库插入多行语句
我们知道通过insert into 表名(列名) values(值)是向表中插入一条语句,可是当我们需要向数据库插入多条语句时,应该怎么做呢? 可以通过如下格式的sql 语句来实现一次向数据库插入多行 ...