`
xitonga
  • 浏览: 579571 次
文章分类
社区版块
存档分类
最新评论

effective C++: 6.继承与面向对象设计

 
阅读更多
六.继承与面向对象设计


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


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


在C++领域中,任何函数如果期望获得一个类型为基类的实参(而不管是传指针或是引用),都也愿意接受一个派生类对象(而不管是传指针或是引用)。(只对public继承才成立。)


好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝”的设计,而不是“运行期才侦测”的设计。
is a并不是唯一存在classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-term-of(根据某物实现出)。


请记住:

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




条款33:避免遮掩继承而来的名称
在继承体系下,派生类的作用域包含在基类作用域下,因而在派生类中的同名变量会遮掩基类的变量。如:
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();
...
};

Derived d;
intx;
...
d.mf1(); //ok,调用Derived::mf1()
d.mf1(x); //error
d.mf2(); //ok,调用Base::mf2
d.mf3(); //ok,调用Derived::mf3
d.mf3(x); //error
在基类中所有名为mf1和mf3的函数都被派生类中的同名函数遮掩掉了,即使它们有不同的参数类型,即使它们是虚函数或非虚函数。


由于上面是public继承,为了能够使用基类中的同名函数(也就是保持is-a关系),可以采用两种办法:使用using声明式或转交函数。
1.在上面的例子中,派生类可做以下修改:
class Derived : public Base{
public:
using Base::mf1; //让Base class内名为mf1和mf3的所有东西中
using Base::mf3; //在Derived作用域中可见
virtual void mf1();
void mf3();
void mf4();
...
};
Derived d;
intx;
...
d.mf1(); //ok,调用Derived::mf1()
d.mf1(x); //OK,调用Base::mf1(int)
d.mf2(); //ok,调用Base::mf2()
d.mf3(); //ok,调用Derived::mf3()
d.mf3(x); //ok,调用Base::mf3(int)
这就意味着如果继承基类并想重载基类函数,而你又希望重新定义或覆写其中一部分,那么为那些原本会被遮掩的每个名称引入以一个using声明式。


2.如果试图选择性地只继承部分重载函数,这在public继承下不可能发生,因为它违反了public继承所暗示的is-a关系,然而在private继承下可能有意义。例如上式Derived以private继承base,而Derived唯一想继承的mf1是那个无参数版本。using声明式这里没用,因为他声明的某给定名称的所有同名函数都


在Derived中可见,可以使用转交函数(forwarding function):
class Derived : private Base{ //私有继承,is-implemented-in-term-of关系
public:
virtual void mf1(){ //转交函数
Base::mf1();
}
...
};

Derived d;
intx;
...
d.mf1(); //ok,调用Derived::mf1()
d.mf1(x); //error,Base::mf1被屏蔽


请记住:
derived calsses内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
为了让被遮掩的名称再见天日,可使用using声明式或转交函数(forwarding function)。




条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。成员函数的接口总是会被继承。


pure virtual函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。所以:声明一个pure virtual函数的目的是为了让derived class只继承函数接口。
令人意外的是,我们竟然可以为pure virtual函数提供定义。但调用它的唯一途径是“调用时明确指出其class名称”:


声明简朴的(非纯)impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现。
声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。
如果成员函数是个non-virtual函数,意味着它并不打算在derived classes中有不同的行为。


请记住:
接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
pure virtual函数只具体制定接口继承。
简朴的(非纯)impure virtual函数具体制定接口继承及缺省实现继承。
non-virtual函数具体制定接口继承以及强制性实现继承。




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


假设你整在写一个视频游戏软件,由于不同的人物可能以不同的方式计算它们的健康指数,将healthValue声明为virtual似乎再明白不过的做法:
class GameCharacter {
public:
virtual int healthValue()const;
...
};
由于这个设计如此明显,你可能没有认真考虑其他替代方案。为了帮助你跳脱面向对象设计路上的常轨,让我们考虑其他一些解法:
1.藉由Non-virtual interface手法实现Template Method模式
有个思想流派主张virtual函数应该几乎总是private。他们建议,较好的设计是保留healthValue为public成员函数,但让它成为non-virtual,并调用一个
private virtual函数(例如doHealthValue)进行实际工作:
class GameCharacter {
public:
int healthValue() const
{
...//做一些事前工作
int retVal = doHealthValue();
...//做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const //derived class 可以重新定义它。
{
...//缺省计算,计算健康指数
}
};
这一基本设计,“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。是所谓templateMethod设计模式的一个独特表现形式。把这个non-virtual函数(healthValue)称为virtual函数的外覆器(wrapper)。
NVI的优点在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器(wrapper)确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等等。
NVI手法涉及在derived class内重新定义private virtual函数。重新定义若干个derived class并不调用的函数!这里并不矛盾。“重新定义virtual函数”表示某些事“如何”被完成,“调用virtual函数”则表示它“何时”被完成。NVI允许derived class重新定义virtual函数,从而赋予它们“如何实现机能”的控制能力,但base class保留诉说“函数何时被调用”的权力。
但又是不能有NVI手法,比如构造函数不能调用虚函数。

2.藉由Function Pointers实现Strategy模式
另一个设计主张是“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
class GameCharacter;
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函数”的做法比较,它提供了某些有趣弹性:
同一人物类型不同实体可以有不同的健康计算函数。例如:
class EvilBadGuy: public GameCharacter {
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCalc)
: GameCharacter(hcf)
{...}
...
};
int loseHealthQuickly(const GameCharacter&);
int loseHealthSlowly(const GameCharacter&);
EvilBadGuy ebg1(loseHealthSlowly);//相同类型的人物搭配
EvilBadGuy ebg2(loseHealthQuickly);//不同的健康计算方式
某已知人物健康指数计算函数可以在运行期变更:例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
这些计算函数并未特别访问“即将被计算健康指数”的那个对象内部成分。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。如果需要non-public信息进行精确计算,就有问题了。唯一能解决“需要以non-member函数访问class的non-public成分”的办法就是:弱化class的封装。例如class可以声明那个non-member函数为friends,或是为其实现的某一部分提供public访问函数。利用函数指针替换virtual函数,其优点(像是“每个对象可各自拥有自己的健康计算函数”和“可在运行期间改变计算函数”)是足以弥补缺点(例如可能必须降低GameCharacter封装性)。


3.藉由tr1::function完成Strategy模式
我们不再用函数指针,而是用一个类型为tr1::function的对象,这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象、或成员数函数指针),只要其签名式兼容于需求端。
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
//HealthCalcFunc可以是任何“可调用物”,可被调用并接受
//任何兼容于GameCharacter之物,返回任何兼容于int的东西。
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
这里我们把tr::function的具现体的目标签名式以不同颜色强调出来。那个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”
std::tr1::function<int (const GameCharacter&)>
所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换成int。
客户在“指定健康计算函数”这件事上有更惊人的弹性:
short calcHealth(const GameCharacter&); //函数return non-int
struct HealthCalculator {//为计算健康而设计的函数对象
int operator() (const GameCharacter&) const
{
...
}
};
class GameLevel {
public:
float health(const GameCharacter&) const;//成员函数,用于计算健康
...
};
class EvilBadGuy : public GameCharacter {
...
};
class EyeCandyCharacter : public GameCharacter {
...
};
EvilBadGuy ebg1(calcHealth);//函数
EyeCandyCharacter ecc1(HealthCalculator());//函数对象
GameLevel currentLevel;
...
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health, currentLevel, _1));//成员函数
GameLevel::health宣称它接受两个参数,但实际上接受两个参数,因为它也获得一个隐式参数GameLevel,也就是this所指的那个。然而GameCharacter的健康计算函数只接受单一参数:GameCharacter。如果我们使用GameLevel::health作为ebg2的健康计算函数,我们必须以某种方式转换它,使它不再接受两个参数(一个GameCharacter和一个GameLevel),转而接受单一参数(GameCharacter)。于是我们将currentLevel绑定为GameLevel对象,让它在“每次GameLevel::health被调用以计算ebg2的健康”时被使用。那正是tr1::bind的作为。


4.古典的Strategy模式
将健康计算函数做成一个分离的继承体系中的virtual成员函数。
class GameCharacter;
class HealthCalcFunc {
...
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;
};
每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。


请记住:
virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。




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

请记住:
绝对不要重新定义继承而来的non-virtual函数。




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


绝不重新定义继承而来的缺省参数值,它有两层意思:
1.如果函数是非虚函数,你根本不应该重新定义它,因此也谈不上改变缺省参数值了。
2.如果函数是虚函数,由于缺省参数值是“静态绑定”,即使使用基类类型的指针或引用指向派生类函数,并且函数以派生类所定义的版本动作,其缺少参数值仍旧是基类所定义的!C++使用这种方式是为了“运行期效率”,在编译期确定参数比在运行期确定效率要高。
对象的所谓静态类型,是它在程序中被声明时所采用的类型。
class 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*
ps,pc,pr不论这些指针指向什么,它们的静态类型都是Shape*
对象的所谓动态类型则是指“目前所指对象的类型”。可以在程序执行过程中改变
ps = pr;
ps = pc;
virtual 函数系动态绑定而来,意思是调用一个virtual 函数时, 究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型:
pc->draw(Shape::Red) //调用Circle::draw
pr->draw(Shape::Red) //调用Rectangle::draw
考虑带有缺省参数的virtual函数时,因为virtual函数是动态绑定的,而缺省参数是静态绑定的。意思是你可能在“调用一个定义于derived class内的virtual函数”的同时,却使用base class 为他所指定的缺省参数值:
pr->draw(); //调用Rectangle::draw(Shape::Red)!
pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数。Rectangle::draw函数的缺省参数应该是Green,但由于pr的静态类型是Shape*,所以调用的缺省参数值来自Shape class而非Rectangle class!
即使把指针换成reference,问题仍然存在。

如果你遵守这条规则,并且同时提供缺省参数值给base和derived classes的用户,又会发生什么呢?
class 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 = Red) const;
};
代码重复。更糟的是,又带有相依性;如果Shape内的缺省参数值改变了,所有“重复给定缺省参数值”的那些derived classes也必须改变,否则它们最终导致“重复定义一个继承而来的缺省参数值”。


解决办法:
当你想令virtual函数表现你所想要的行为却遭遇麻烦,聪明的做法是考虑替代设计。条款35。其中之一就是NVI手法:
令base class内的一个public non-virtual函数调用一个private virtual函数,后者可被derived classes重新定义。这里我们可以让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作:
class Shape {
public:
enum ShapeColor{Red, Green, Blue};
void draw(ShapeColor color = Red) const
{
doDraw(color);
}
private:
virtual void doDraw(ShapeColor color) const = 0;//纯虚函数
};
class Rectangle : public Shape {
public:
private:
virtual void doDraw(ShapeColor color) const;//不需指定缺省参数
};
由于non-virtual函数应该绝对不被derived classes覆写(条款36),这个设计很清楚的使draw函数的color缺省参数值总为Red。


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




条款38:通过复合塑模出has-a或“根据某物实现出”
public继承是“is-a“的关系,而复合有”has-a“或”根据某物实现出(is-implemented-in-terms-of)“的意思——当复合发生在应用域内的对象之间,表现出has-a关系;当它发生于实现域内则是表示“根据某物实现出”的关系。


请记住:
复合(composition)的意义和public继承完全不同。
在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。


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


c++中public继承视为is-a关系。现在看private继承:
class Person{...};
class Student: private Person {...};
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p);
eat(s); //错误! 难道学生不是人?!
显然private继承不是is-a关系。


1.如果class之间的继承关系是private,编译器不会自动将一个derived对象转换为一个base对象。
2.由private base class继承而来的所有成员,在derived class中都会成为private属性,纵使它们在base class中原本是protected或public。


private继承纯粹只是一种实现技术,private继承意味着只有实现部分被继承,接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得。


private继承意味着implemented-in-term-of(根据某物实现)。条款38说复合(composition)的意义也是is-implemented-in-term-of,如何进行取舍?


尽可能的使用复合,必要时才使用private继承:主要是当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分,或为了重新定义virtual函数,还有一种激进情况是空间方面的厉害关系。


有个Widget class,它记录每个成员函数的被调用次数。运行期周期性的审查那份信息。为了完成这项工作,我们需要设定某种定时器,使我们知道收集统计数据的时候是否到了。
为了复用既有代码,我们发现了Timer:
class Timer {
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
};
每次滴答就调用某个virtual函数,我们可以重新定义那个virtual函数,让后者取出Widget的当时状态。
为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但public继承并不适当,因为Widget并不是一个Timer。不能够对一个Widget调用onTick吧,观念上那并不是Wigdet接口的一部分。


我们必须用private继承Timer:
class Widget: private Timer{
private:
virtual void onTick() const;
};
再说一次,把onTick放进public接口内会导致客户以为他们可以调用它,那就违反了条款18.


这个设计好,但不值几文钱,private继乘并非绝对必要。如果我们决定用复合取而代之,是可以的,只要在Widget内声明一个嵌套式private class,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型的对象在Widget内:
class Widget{
private:
class WidgetTimer: public Timer{
public:
virtual void onTick() const;
};
WidgetTimer timer;
};
这个设计比只是用private继承复杂一些,但是有两个理由可能你愿意或应该选择这样的public继承加复合:
首先,或许会想设计Widget使它得以拥有derived classes,但同时你可能会想阻止derived clssses 重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。(条款35说derived class可以重新定义virtual函数,即使它们不得调用它)但如果WidgetTimer是Widget内部的一个private成员并继承Timer,Widget的derived classes将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。
第二,或许想要将Widget的编译依存性降至最低,若Widget继承Timer,当Widget被编译时,timer的定义必须可见,所以定义Widget的那个文件必须包含Timer.h。但如果WidgetTimer移除Widget之外而Widget内含一个指针指向WidgetTimer,Widget可以只带着一个简单的WidgetTimer声明式,不再需要#include任何与timer有关的东西。对大型系统而言,如此的解耦(decouplings)可能是重要的措施。


还有一种激进情况,只适用于你所处理的class不带任何数据时。这样的classes没有non-static成员变量,没有virtual函数(这种函数会为每个对象带来一个vptr),也没有virtual base classes(这样的base classes 也会招致体积上的额外开销,见条款40)。这种所谓的empty class对象不使用任何空间,因为没有任何隶属对象的数据要存储,然而由于技术上的理由,c++裁定凡是独立(非附属)对象都必须有非零大小:
class Empty{};
class HoldsAnInt {
private:
int x;
Empty e; //应该不需要任何内存
};
你会发现sizeof(HoldsAnInt)> sizeof(int);一个Empty成员变量竟然要求内存。在多数编译器中sizeof(Empty)获得1,因为面对“大小为零的独立对象”,通常c++官方勒令默默安插一个char到空对象内。然而齐位需求(alignment)可能造成编译器为类似HoldsAnInt这样的class加上一些衬垫(padding),所以有可能HoldsAnInt对象不只多一个char大小,实际上放大到多一个int。

独立(非附属)这个约束不适用于derived class对象内的base class成分,因为它们并非独立。如果你继承Empty,而不是内含一个那种类型的对象:
class HoldsAnInt: private Empty{
private:
int x;
};
几乎可以确定sizeof(HoldsAnInt) == sizeof(int)。这是所谓的EBO(empty base optimization:空白基类最优化),如果你是一个库开发成员,而你的客户非常在意空间,那么值得注意EBO。另外一个值得知道的是,一般EBO只在单一继承(而非多继承)下才可行。
现实中的“Empty”class并不是真的empty。虽然他们从未拥有non-static 成员变量,却往往内含typedefs, enums, static成员变量,或non-virtual函数。stl就有许多技术用途的empty classes,其中内含有用的成员(通常是typedefs),包括base classes unary_function和binary_function,这些是“

用户自定义的函数对象”通常都会继承的classes。感谢EBO的广泛实践,这样的继承很少增加derived classes的大小。
尽管如此,大多数class并非empty,所以EBO很少成为private继承的正当理由。复合和private继承都意味着is-implemented-in-term-of,但复合比较容易理解,所以无论什么时候,只要可以,还是应该选择复合。


请记住:
1.private继承意味is-implementation-in-terms of(根据某物实现出)。她通常比复合级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
2.和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。


条款40:明智而审慎地使用多重继承
一旦涉及多重继承(multiple inheritance;MI):
程序有可能从一个以上的base class继承相同名称(如函数、typedef等)。那会导致较多的歧义机会。例如:
class BorrowableItem {
public:
void checkOut();
};
class ElectronicGadet {
private:
bool checkOut() const;
};
class MP3Player: public BorrowableItem
public ElectronicGadet
{...};
MP3Player mp;
mp.checkOut();//歧义,调用的是哪个checkOut?


即使两个之中只有一个可取用(ElectronicGadet是private)。这与c++用来解析重载函数调用的规则相符:在看到是否有个函数可取之前,c++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配才检验其可取用性。本例的两个checkOut有相同的匹配程度。没有所谓最佳匹配。因此ElectronicGadget::checkOut的可取用性就从未被编译器审查。

为了解决这个歧义,必须明白指出你要调用哪个base class内的函数:
mp.BorrowableItem::checkOut();
你当然也可以明确调用ElectronicGadget::checkOut(),但然后你会获得一个“尝试调用private成员函数”的错误。
当即称一个以上的base classes,这些base classes并不常在继承体系中有更高级的base classes,因为那会导致要命的“钻石型多重继承”:
class File{...};
class InputFile: public File {...};
class OutputFile: public File{...};
class IOFile: public InputFile,
public OutputFile
{...};
任何时候只要你的继承体系中某个base class和某个derived class之间有一条以上的想通路线,你就必须面对这样一个问题:是否打算让base class内的成员经由每一条路径被复制?假设File有个成员变量fileName,那么IOFile应给有两份fileName成员变量。但从另一个角度来说,简单的逻辑告诉我们,IOFile对象只有一个文件名称,所以他继承自两个base class而来的fileName不能重复。


c++的缺省做法是执行重复。如果那不是你要的,你必须令那个带有此数据的base class(也就是File)成为一个virtual base class。必须令所有直接继承自它的classes采用“virtual继承”:
class File{...};
class InputFile: virtual public File {...};
class OutputFile: virtual public File{...};
class IOFile: public InputFile,
public OutputFile
{...};
c++标准程序库内含一个多重继承体系,只不过其class是class template: basic_ios,basic_istream,basic_ostream和basic_iostream。
从正确行为来看,public继承应该总是virtual。如果这是唯一一个观点,规则很简单:任何时候当你使用public继承,请改用virtual public继承。但是,正确性并不是唯一观点。为避免继承来的成员变量重复,编译器必须提供若干幕后戏法,其后果就是:使用virtual继承的那些classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes成员变量速度慢。
virtual继承的成本还包括其他:支配“virtual base classes初始化”的规则比起non-virtual base的情况远为复杂和不直观。virtual base 的初始化责任是由继承体系中的最底层(most derived)class负责,1、class若派生自virtual base class而需要初始化,必须认知其virtual bases----不论那些bases距离多远,2、当一个新的derived class加入继承体系中,它必须承担起virtual bases(不论直接或间接)的初始化工作。

我们对virtual继承的忠告:第一,非必要不要使用virtual bases。第二,如果必须使用virtual bases,尽可能避免在其中放置数据。这样你就不需担心这些classes身上的初始化(和赋值)所带来的诡异事情了。

请记住:
1.多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需求。
2.virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
3.多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。
分享到:
评论

相关推荐

    Effective C++ 中文版

    6.继承与面向对象设计 条款32:确定你的public继承塑模出is-a关系 条款33:避免遮掩继承而来的名称 条款34:区分接口继承和实现继承 条款35:考虚virtual函数以外的其他选择 条款36:绝不重新定义继承而来的non...

    Effective C++

    5、继承与面向对象设计 条款35:使公有继承体现是一个的函义 条款36:区分接口继承与实现继承 条款37:绝不要重新定义继承而来的非虚函数 条款38:绝不要重新定义继承而来的缺省参数值 条款39:避免向下转换继承层次 条款...

    Effective.C++.中文第二版.50条款doc文档.chm

    第六章 继承和面向对象设计 条款35: 使公有继承体现 "是一个" 的含义 条款36: 区分接口继承和实现继承 条款37: 决不要重新定义继承而来的非虚函数 条款38: 决不要重新定义继承而来的缺省参数值 条款39: 避免 "向下...

    Effective C++(第三版)

    6. 继承与面向对象设计 inheritance and object-oriented design 条款32:确定你的public继承塑模出is-a关系 make sure public inheritance models “is-a.” 条款33:避免遮掩继承而来的名称 avoid hiding ...

    Effective C++中文版

    Effective C++中文版,包括C、C++内存管理,设计与声明,类和函数的实现,继承和面向对象的设计等

    OOD启思录 高清pdf

    而《OOD启思录》被读者评价为“面向对象设计领域中的Effective C++”——正如Effective C++能帮助你迈向C++专家层面,《OOD启思录》能帮助你迈入OOD殿堂。 本书提供了改进面向对象设计的真知灼见。  全书共11章...

    Effective c++

    提高编程效率的50条建议 条款1:尽量用const和inline而不用#define 条款2:尽量用而不用&lt;stdio.h&gt; 条款3:尽量用new和delete而不用malloc和free 内存管理的建议 设计与说明的建议 继承与面向对象的设计 杂项

    c++ 面向对象的类设计

    好像,more effective c++上说的,class的成员函数,应该是在完整的情况下保持最小化。但是,这里我们的出发点,是成员数据的完整最小化。 最小化的好处是可以保持概念最大的独立性,也意味着,可以用最小的代价来...

    C++书籍集合

    本书专注于C++面向对象程序设计的底层机制,包括结构式语意、临时性对象的生成、封装、继承,以及虚拟——虚拟函数和虚拟继承,帮助你理解程序的底层实现,以便写出更高效的代码。 《The design and evolution of ...

    程序员面试刷题的书哪个好-CPP_Learning:记录下C++语言学习

    面向对象编程 从C到C++ 封装 继承 多态 操作符重载 模版与泛型编程 输入输出 经典书籍 C++ Primer Effective三部曲 STL源码剖析 面试问题记录 from 牛客, 我现在的想法是C/C++方向,涉及到具体的知识有数据结构与...

    asp.net知识库

    与DotNet数据对象结合的自定义数据对象设计 (一) 数据对象与DataRow ASP.NET中大结果集的分页[翻译] .net 2.0 访问Oracle --与Sql Server的差异,注意事项,常见异常 Ado.net 与NHibernate的关系? 动态创建数据库...

Global site tag (gtag.js) - Google Analytics