C++的OOP有可能与你原本习惯的OOP稍有不同:继承可以是单一继承或多重继承,每一个继承连接link可以是public、protected或private,也可以是virtual或non-virtual。然后成员函数的各个选项:virtual?或non-virtual?以及成员函数和其他语言特性的相互影响:缺省参数与virtual函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择么?
本节解释C++各种不同特性的真正意义,也就是当你使用某个特定构件你真正想要表达的意思。例如public继承意味着is-a关系。virtual函数意味着接口必须被继承,non-virtual函数意味“接口和实现必须都被继承”。
如果你了解C++的各种特性的意义,你对OOP的看法改变了。它不再是一项用来区别语言特性的仪典,而是可以让你通过它说出你对软件系统的想法。一旦你知道该通过它说些什么,转移至C++世界也就不再是可怕的高要求了。
条款32: 确定你的public继承塑模出is-a关系
以C++进行的面向对象编程,最重要的一个规则是:public inheritance意味着is-a关系。
public继承和is-a之间等价关系听起来颇为简单,但有时候你的直觉可能会误导你。例如,企鹅是鸟。鸟可以飞,但企鹅不能飞。
class Bird
{
public:
virtual void fly(); // 声明fly函数
...
};
class Penguin : public Bird
{
...
};
// 修改为
class Bird
{
... // 没有声明fly函数
};
class FlyBird : public Bird
{
public:
virtual void fly();
...
};
class Penguin: public Bird
{
... // 没有声明fly函数
}
修改后的继承体系比原先设计更能忠实反映我们正在的意思。另一种思想流派是处理“所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞”,就是为企鹅重新定义fly函数,令它产生一个运行期错误:
void error (const std::string& msg); // 定义于它处
class Penguin : public Bird
{
public:
virtual void fly() { error("Attempt to make a penguin fly!"); }
...
};
两种方式的差异在于,从错误被侦查出来的时间点观之,“企鹅不会飞”这一限制可由编译期强制实施,但若违反“企鹅尝试飞行,是一种错误”这一规则,只有运行时才能检测出来。条款18说过,好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期拒绝企鹅飞行”的设计,而不是“只在运行期才能侦测它们”的设计。
考虑以下代码:
class Rectangle
{
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const;
virtual int width() const;
...
};
void makeBigger(Rectangle& r)
{
int oldHeight = r.height();
r.setWidth(w.width() + 10);
assert(r.height() == oldHeight);
}
class Square : public Rectangle {...};
Square s;
...
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height);
public主张,能够施行于base class对象身上的每件事情,也可以施行于derived class对象身上。
is-a并非唯一存在于classes之间的关系,另外两个常见的关系是has-a和is-implemented-in-term-of。将上述任何重要的相关关系中的任何一个塑造成is-a而造成的错误设计,在C++中并不罕见。
请记住:
“public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived class身上,因为每一个derived class也是一个base class对象
条款33: 避免遮掩继承而来的名称
int x; // global 变量
void someFunc()
{
double x; // local 变量
std::cin >> x;
}
C++的名称遮掩规则(name-hiding rules)所做的唯一事情就是:遮掩名称。至于名称是否应和相同或不同的类型,并不重要。
现在导入继承,我们知道,当位于一个derived class成员变量函数内refer to base class内的某物时,编译器看找出我们refer to的东西,因为derived class继承了声明于base class内的所有东西。实际运作方式是derived class作用域被嵌套在base class作用域内。
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf2();
void m3();
...
};
class Derived : public Base
{
public:
virtual void mf1();
void mf4();
...
};
假设derived class内的mf4的实现代码如下;
void Derived::mf4()
{
...
mf2();
...
}
当编译器看见mf2,必须evaluate它refer to什么东西,编译器的做法是查找各作用域,看看有没有某个mf2的声明式。首先查找local作用域(也就是mf4覆盖的作用域),若没找到。于是查找其外围作用域,也就是class Derived覆盖的作用域。若依旧没找到,于是再往外围移动,本例为base class。此时编译器找到一个名为mf2的东西了,于是停止查找。如果Base内还是没有,查找动作则继续下去,首先查找含有Base的那个namespace作用域(如果有的话),否则往global作用域查找、
再次考虑前一个例子,重载mf1和mf3,并且添加了一个新版mf3到Derived去。此时发生的事情是:Derived重载了mf3,那是一个继承而来的non-virtual,这样会使得整个设计疑云重重。
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void m3();
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 d;
int x;
...
d.mf1(); // 调用Derived::mf1
d.mf1(x); // 错误,Derived::mf1遮掩了Base::mf1
d.mf2(); // 调用Base::mf2;
d.mf3(); // 调用Derived::mf3
d.mf3(x); // 错误,因为Deirved::mf3遮掩了Base::mf3
即使base class和derived class内的函数有不同的参数类型也适用,而且不论函数是virtual或non-virtual一体适用。
这种行为的背后的基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带得从疏远的base class继承重载函数。
但是你可以使用using声明式来推翻(override)C++对“继承而来的名称”的缺省遮掩行为。
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; // 让Base class内mf1和mf3的所有东西都在
using Base::mf3; // Derived作用域都可见(并且public)
virtual void mf1();
void mf3();
void mf4();
...
};
Derived d;
int x;
...
d.mf1(); // 调用Derived::mf1
d.mf1(x); // OK,调用Base::mf1
d.mf2(); // 调用Base::mf2;
d.mf3(); // 调用Derived::mf3
d.mf3(x); // OK,调用Base::mf3
这意味如果你继承base class并加上重载函数,而你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using声明,否则这些你希望继承的名称会被遮掩掉。
有时候你并不想继承Base class内的所有函数,这是可以理解的。在public继承下,这绝对不可能发生,因为它违反了base的derived class之间的is-a关系。
例如假设Derived以private形式继承Base,而Derived唯一想要继承的mf1是无参数版本。using声明在此排不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。
此时,我们需要不同的技术,即一个简单的转交函数(forwarding function):
class Base
{
public:
virtual void mf1() = 0;
virtual mf1(int);
...
};
class Derived : public Base
{
public:
virtual void mf1() { Base::mf1();} 转交函数暗自成为inline。
};
...
Derived d;
int x;
d.mf1(); // 调用derived::mf1;
d.mf1(x); // 错误,Base::mf1(int x)被遮掩
inline转交函数的另一个用途是为那些不支持using声明式的老旧编译器开辟一条新路,将继承而得的名称汇入derived class作用域内。
请记住:
derived class内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此。
为了让被遮掩的名称再见天日,可使用using 声明式或转交函数。
条款34: 区分接口继承和实现继承
public的继承挂念,发现是由两部分组成:函数接口(function interface)继承和函数实现(function implementation)继承。
身为class的设计者,有时候你会希望derived class只继承成员函数的接口;有时希望derived class同时继承接口和实现,但有时候希望能够override它们所继承的实现;有时候希望derived class同时继承函数的接口和实现,并且不允许覆写任何东西。
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是一个抽象class,它的pure virtual函数draw使它成为一个抽象class。Shape强烈影响所有以public继承它的derived class,因为:
成员函数的接口总是会被继承;
pure virtuall函数有两个最突出的特性:它们必须被任何“继承了他们”的具现class重新声明,而且它们在抽象class通常没有定义。此时就明白 声明一个pure virtual函数的目的为了让derived class只继承函数接口。Shape::draw的声明式对具象derived classes设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它”。
令人意外的是,我们也可以为pure virtual函数提供定义,但是调用他的唯一途径是“调用时明确指明其class的名称”。
Shape* ps = new Shape; // 错误! Shape是抽象的
Shape* ps1 = new Rectangle;
ps1->draw(); // 调用Rectangel::draw
Shape* ps2 = new Ellipse;
ps2->draw(); // 调用Ellipse::draw
ps1->Shape::draw(); // 调用Shape::draw
ps2->Shape::draw(); // 调用Shape::draw
给抽象基类的纯虚函数提供定义的方式可以实现一种机制,为impure virtual函数提供更平常更安全的缺省实现。
声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现;
class Shape
{
virtual void error(const std::string& msg);
...
};
Shape::error的声明式告诉derived classes的设计者,“你必须支持一个error函数,但如果你不想直接写一个,可以使用Shape class的缺省版本”。
但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。比如XYZ航空公司的飞机继承体系,A型和B型两种飞机都以相同方式飞行。
class Airport{...};
class Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
缺省代码,将飞机飞至指定目的地
}
class ModelA : public Airplane {...};
class ModelB : public Airplane {...};
// 假设XYZ公司决定购买一种新式C型飞机。C型和AB两者飞行方式不同
class ModelC : public Airplane {...};
// 然后代码有一些诸如此类动作
Airport* PDX(...);
Airplane* pa = new ModelC;
...
pa->fly(PDX); // 调用Airplane::fly
这将酿成大灾难:试图以ModelA或ModelB的飞行方式来飞ModelC。
问题不在于Airplane::fly有缺省行为,而在于ModelC在未明白“我要”的情况下就继承了该缺省行为。幸运的是我们可以轻易做到“提供缺省实现给derived class,但除非它们明白要求否则免谈”。此技术在于切断“virtual函数接口”和其“缺省实现”的连接,下面是一种做法:
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
缺省行为,或将飞机飞至指定目的地
}
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); }
...
};
如此ModelC不可能意外继承不正确的fly实现代码。此时ModelC必须提供自己的fly版本
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination);
...
};
void MedelC::fly(const Airport& destination)
{
将C型飞机飞至指定目的地
}
Airplane::defaultFly是个non-virtual函数,这一点很重要,因为没有任何一个derived class应该重新定义此函数。
有人反对以不同的函数分别提供接口和缺省实现,像上述fly和defaultFly那样。它们关心因过度雷同的函数名称引起的class命名空间污染问题,但是它们也同意,接口和缺省实现应该分开。此时我们可以利用“pure virtual”函数必须在derived class中重新声明,但它们也可以拥有自己的实现。
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::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);
...
};
void MedelC::fly(const Airport& destination)
{
将C型飞机飞至指定目的地
}
现在的fly被分割为两个基本要素:其声明部分表现的是接口(那是derived class必须使用的),其定义部分则表现出缺省行为(那是derived class可能使用的,但只有它们明确提出申请时才是)。
最后让我们看看Shape的non-virtual函数ObjectID。
class Shape
{
public:
int objectID() const;
...
};
如果成员是个non-virtual函数,意味着它并不打算在derived class中有不同的行为。实际上一个non-virtual成员函数所表现出来的不变形(invariant)凌驾其特异性(specialization),因为它表示不论derived class变得多么特异化。
声明non-virtual函数的目的是令derived classes继承函数接口以及一份强制性实现。
pure virtual函数,impure virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要的的derived class继承的东西:只继承接口、或是继承接口和一份缺省实现、或是继承接口和一份强制实现。
class经常犯的两个错误:
第一个错误是将所有函数声明为non-virtual。这使得derived class没有余地进行特化工作。non-virtual析构函数尤其会带来问题。当然了,设计一个并不想成为base class的class是绝对合理的。将其所有成员函数都声明为non-virtual,这种声明如果不是忽略了virtual函数和non-virtual函数之间的差异,就是过度担心virtual函数的效率成本。
另一个常见的额错误是将所有成员函数声明为virtual。如果你的不变形凌驾特异性,别害怕说出来。
请记住:
接口继承和实现继承不同。在public继承之下,dderived class总是继承base class的接口。
pure virtual函数只具体指定接口继承。
impure virtual函数具体指定接口继承及缺省继承。
non-virtual函数具体指定接口继承以及强制性实现。
条款35: 考虑virtual函数以外的其他选择
由于不同的人物可能以不同的方式计算他们的健康指数,将其声明为virtual似乎是再明白不过的做法:
class GameCharacter
{
public:
virtual int healthValue() const;
...
};
让我们考虑一下其他解法;
藉由non-virtual interface手法实现Template Method模式:
此流派主张virtual函数应该几乎总是private。他们认为较好的设计是保留healthValue为public成员函数,但 让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:
class GameCharacter
{
public:
int healthValue() const // derived classes 不重新定义它
{
... // 做一些事前工作
int retVal = doHealthValue(); // 做真正工作
... // 做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const // derived class可重新定义它
{...}
};
这一基本设计,也就是“令客户通过public non-virtual成员函数间接调用virtual函数”,成为non-virtual interface(NVI)手法。它是Template Method的一种独特表现形式。我把这个non-virtual函数称为virtual函数的外覆器(wrapper)。
NVI手法的一个优点是隐身在上述代码注释“做一些事前工作”和“做一些事后工作”之中。这意味着外覆器确保在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。
NVI手法涉及在derived class内重新定义private virtual函数。重新定义virtual函数表示某些事是如何被完成的,调用virtual函数则表示它何时被完成。C++的这种derived class可重新定义继承而来的private virtual函数的规则则完全合情合理。
在NVI手法下其实没有必要让virtual函数一定是private。某些class继承体系要求derived class在virtual函数的实现内必须调用其base class的对应兄弟,二纬路让这样的调用合法,virtual函数必须是protected,不能是private。有时候virtual函数甚至一定得是public。
藉由Function Pointer实现Strategy模式
例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
class GameChracter;
int defaultHealthCal(const GameChracter& gc);
class GameChracter
{
public:
typedef int (*HealthCalcFunc)(const GameChracter&);
explicit GameChracter(HealthCalcFunc hcf = defaultHealthCal)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
private:
HealthCalcFunc healthFunc;
};
它提供了某些有趣的弹性:
同一人物类型之不同实体可以有不同的健康计算函数,例如:
class EvilBadGuy : public GameChracter
{
public:
explicit EvilBadGuy(HealthCalcFunc hcf = defaultHealthCal)
: GameChracter(hcf)
{...}
...
};
int loseHealthQuickly(const GameChracter&);
int loseHealthSlowly(const GameChracter&);
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
某已知人物值健康指数函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,同来替换当前的健康指数计算函数。
换句话说,“健康指数计算函数不再是GameCharacter继承体系内的成员函数这一事实意味,这些计算函数并未特别访问“即将被计算健康指数”的那个对象的内部成分。例如defaultHealthCalc并未访问EvilBadGug的non-public成分。
一般而言,唯一能够解决“需要以non-member”函数访问class的non-public成员的办法就是:弱化class的封装。例如class可声明某个non-member函数为friends,或是为其实现的某一部分提供public访问函数(其他部分则宁可因此起来)。运用函数指针替换virtual函数。其优点(像是“每个对象可各自拥有自己的健康计算函数”)和“可在运行期改变计算函数”是否足以弥补缺点,是你必须根据每个设计情况的不同而抉择的。
藉由tr1::function完成Strategy模式
一旦习惯了template以及它们对隐式接口的使用,基于函数指针的做法看起来便过分苛刻而死板了。为什么要求“健康指数值计算”必须是个函数,而不能是某种“像函数的东西”例如函数对象呢?如果的是个函数,为什么不能是成员函数呢?为什么一定得返回int而不是任何可转换为int的类型呢?
如果不在使用函数指针,而是使用tr1::function的对象,这些约束全都挥发不见了、这样的对象可持有(保存)任何可调用物(callable entity,也就是函数指针、函数对象或成员函数指针),只要其签名是兼容于需求端。
class GameCharacter;
int defaultHealthCal(const GameChracter& gc);
class GameCharacter
{
public:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCal)
: healthFunc(hcf)
{}
int healthValue() const
{ return healthFunc(*this); }
...
private:
HealthCalcFunc healthFunc;
};
这个tr1::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。和前一个函数指针实现而言,唯一不同在于GameCharater持有的是tr1::functon对象,相当于一个指向函数的泛化指针。
古典Strategy模式
class GameCharacter;
class HealthCalcFunc
{
public:
...
virtual int clac(const GameCharacter& gc) const
{...}
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCal)
: pHeathCalc(phcf)
{}
int healthValue() const
{ return pHealthCalc->calc(*this); }
...
private:
HealthCalcFunc* pHealthCalc;
};
快速重点复习我们验证过的几个替代方案:
使用non-virtual interface手法(NVI),那是Template Method设计模式的一种特殊形式。它以public non-vritual成员函数包装较低访问性(private或protected)的virtual函数。
将virtual函数替换为“函数指针成员变量”,这是Strategy的一种分解表现形式。
以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
将继承体系的virtual函数替换为另一个继承体系的virtual函数,这是Strategy设计模式的传统实现手法。
请记住:
virtual函数的替换方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是Template Method的设计模式。
将机能从成员函数转移到class外部的函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定值目标签名式兼容”的所有可调用物。
条款36: 绝不重新定义继承而来的non-virtual函数
class B
{
public:
void mf();
...
};
class D : public B {...};
D x;
B* pB = &x;
D* pD = &x;
// 以下两个mf调用可能是不一样的
pB->mf();
pD->mf();
如果mf是一个non-virtual函数而D定义有自己的mf版本,则mf的调用就不一样了。
class D
{
public:
void mf();
...
};
造成此两面行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定。这意味着pB声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向类型为B的派生类对象。
另一方面,virtual函数缺失动态绑定(dynamic bind),所以他们不受这个问题之苦。
如果派生类D重新定义了继承自基类的non-virtual函数mf,派生类对象D可能展现出精神分裂的不一致行为。更明确地说,当mf被调用时,任何一个派生类对象都可能表现出基类或派生类的行为:决定因素不在于对象自身,而在于“指向该对象之指针”当初的声明类型。
条款32说明所谓的public继承意味着is-a关系。条款34则描述为什么在class内声明一个non-virtual函数会为该class建立起一个不变形invariant,凌驾其specialization。运用这两个观点,那么:
适用于B对象的每一件事,也适用于D对象,因为每一个D对象都是一个B对象;
B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数;
如果D重新定义了mf,则每个D都为B就不为真了。并且,如果D真的有需要实现出与B不同的mf,那么mf就无法为B反映出“不变形凌驾特异性”的性质。
不论哪一个观点,结论都相同:任何情况下都不该重新定义一个继承而来的non-virtual函数。
请记住:
绝对不要重新定义及成果而来的non-virtual函数。
条款37: 绝不重新定义继承而来的缺省参数值
因为不会重新定义继承而来的non-virtual函数。所以本条款局限于继承一个带有缺省参数值的virtual函数。virtual函数系动态绑定(dynamic bound),而缺省参数值却是静态绑定(statically bound)。
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。
class Shape
{
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle : public Shape
{
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle : public Shape
{
virtual void draw(ShapeColor color) const;
};
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
对象的所谓动态类型(dynamic type)则是指“目前所指对象的类型”。动态类型一如其名所示,可在执行过程中改变;
ps = pc;
ps = pr;
virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用那一份函数实现代码,取决于发出调用的那个对象的动态类型。当考虑带有缺省参数的virtual函数,花样来了。
若pr->draw执行时,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数。pr的静态类型是Shape,所以此一调用的缺省参数值来自于Shape而非Rectangle。结局是这个函数调用有着奇怪并且几乎绝对没人预料到的组合,由Shpae和Rectangle的draw各出一半力。重点在于draw是个virtual函数,而它有个缺省的参数值在derived class中被重新定义了。
为什么C++坚持以这种乖张的方式来运作呢?答案在于运行效率。如果缺省参数是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译器决定”的机制更慢而且更复杂。
如果尝试遵守本条款,并且同时提供默认缺省参数值给base和derived class的用户,会发生什么事情呢?
class Shape
{
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle : public Shape
{
virtual void draw(ShapeColor color = Red) const;
...
};
此时,代码重复,更糟的是,代码重复又导致相依性。如果Shape内的缺省参数值改变了,所有重复给定参数值的那些derived class也必须改变 ,否则它们最终会导致重复定义一个继承而来的缺省参数值。怎么办? 可以使用NVI(no-virtual interface)手法。令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived class重新定义。
class Shape
{
public:
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const
{
doDraw(color);
}
...
private:
virtual void doDraw(ShapeColor) const = 0 ;
};
class Rectangle : public Shape
{
public:
...
private:
virtual void doDraw(ShapeColor color) const;
...
};
请记住:
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数--你唯一应该覆写的东西--却是静态绑定。
条款38: 通过复合塑模出has-a关系或根据某物实现出
复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:
class Address {...};
class PhoneNumber {...};
class Person
{
public:
...
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
};
当复合关系发生于应用域内的对象之间,表现出has-a关系;当它发生于实现域内则表现出is-implemented-in-terms-of的关系。
set的实现往往招致“每个元素耗用3个指针”的额外开销。因为sets通常以平衡查找树实现而来的。当速度比空间重要,这是个合理的设计。
set是一种list并不为真,因为对list对象为真的某些事情对Set对象并不为真。由于这两个class之间并非is-a关系,所以public关系继承不适合用来塑模它们。
template<class T>
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep;
};
Set成员函数大量依赖list以及标准库其他部分提供的机能来完成,所以其实现很直观也很简单。
template <typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(), rep.end(), item) != rep.end()
}
template<tyepname T>
void Set<T>::insert(const T& item)
{
if (!member(iter)) rep.push_back(item);
}
template <typename T>
void Set<T>::remove(const T&)
{
typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item);
if (it != rep.end()) rep.erase(it);
}
template <typename T>
std::size_t Set<T>::size() const
{
return rep.size();
}
请记住:
复合(composition)的意义和public继承完全不同。
在应用域,复合意味着has-a。在实现域,复合意味着is-implemented-in-terms-of。
条款39: 明智而谨慎地使用private继承
若我们将Student以private形式继承Person,则:
class Person {...};
class Student : private Person {...};
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
//...
eat(p);
eat(s); // 错误!
如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。第二条规则是,由private base class继承而来的所有成员,在derived class都会变成private属性,纵使它们在base class中原本是protected或public属性。
private继承意味着implemented-in-terms-of。如果你让class D以private形式继承class B,你的用意是为了采用class B内已经备好的某些特性,不是因为B对象和D对象存在任何观念上的关系。private继承纯粹只是一种实现技术。
Private继承在软件“设计”层面上没有意义,其意义只涉及软件的实现层面。
has-a和private 之间取舍时候,尽可能使用符合,必要时才使用private继承,何时才是必要?主要是当protected成员和/或virtual函数牵涉进来的时候。其实还有一些激进情况,那是当空间的利害关系足以踢翻private继承的支柱之时。
class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
...
};
// 若以private继承Timer
class Widget : private Timer
{
private:
virtual void onTick() const;
...
};
// public继承+复合
class Widget
{
private:
class WidgetTime : public Timer
{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
我可以想到两个理由,为什么你可能愿意选择public继承加复合,而不是原先的private继承设计。首先,你或许会想到设计Widget使它得以拥有derived class,但同时你可能会向阻止derived class重新定义onTick。如果WidgetTimer是Widget内部一个private成员并继承Timer,Widget的derived class将无法使用WidgeTimer,因此无法继承它或重新定义它的virtual函数。这实现了阻止derived class重新定义virtual函数的能力。
第二,你或许想要将Widget的编译依存性降至最低。如果Widget继承Timer,则Widget编译时Timer的定义必须可见。对于大型系统而言,如此的解耦可能是重要的措施。
private继承主要用于“当一个意欲成为derived class者”想要访问一个意欲成为base class者的protected成分,或为了重新定义一个或多个virtual函数。
激进情况是只适用于你所处理的class不带任何数据时。这样的class没有non-static成员变量,没有virtual函数,也没有virtual base classes。于是这种所谓的empty classes对象不使用任何空间,因为没有任何隶属对象的数据需要存储。
class Empty;
class HoldsAnInt
{
private:
int x;
Empty e;
};
在大多数编译器中空类是一个字节大小,因为面对大小为零值独立对象,通常C++官方勒令默默安插一个char到空对象内。然而齐位要求可能造成编译器为类似HoldsAnInt加上一些padding。
class HoldsAnInt : private Empty
{
private:
int x;
};
若Holds私有继承Empty,则sizeof(HoldsAnInt) = sizeof(int),这就是所谓的EBO(Empty Base Optimization,空白基类最优化)。EBO一般只有在单一继承(而非多重继承)下才可行,统治C++对象布局的那些规则通常表示EBO无法被施行于“拥有多个base”的derived class身上。
当你面对“并不存在is-a关系”的两个class,其中一个需要访问另一个的protected成员,或需要重新定义其一个或多个virtual函数,private继承极有可能成为正统设计策略。即便如此,一个混合public继承和复合的设计,往往也能实现你想要的行为。
请记住;
private继承意味着is-implemented-in-terms-of。它通常比复合的级别低。但是当derived class需要访问protected base的成员,或重新定义继承而来的virtual函数时,这么设计是合理的。
和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
条款40: 明智而谨慎地使用多重继承
多重继承(multiple inheritance,MI)进入设计镜框时,程序有可能从一个以上的base class继承相同的名称(如函数、typedef等等)。那会导致较多歧义机会。例如:
class BorrowableItem
{
public:
void checkOut();
...
};
class EletronicGadget
{
private:
bool checkOut() const;
...
};
class MP3Player : public BorrowableItem, public EletronicGadget
{
...
};
MP3Player mp;
mp.checkOut(); // 歧义,调用谁的checkOut
在看到是否有个函数可取用之前,C++首先确认这个函数对比调用之言是最佳匹配。找出最佳匹配之后才检验其可取用性。
为了解决歧义,你必须指明白你要调用哪个base class内的函数。
mp.BorrowableItem::checkOut();
多重继承的意思是继承一个以上的base class,但这些base class并不常在继承体系中又有更高级的base class,因为那会导致要命的钻石型多重继承。
任何时候如果你有一个继承体系而其中某个base class和某个derived clas之间有一条以上的相通路线,你就必须面对一个问题是否打算让base class内成员变量经由每一条路线被复制?
C++在这个问题中并没有倾斜立场,两个方案都支持--虽然缺省的做法是执行复制,如果那不是你想要的的,你必须令那个带有此数据的class成为一个virual base class。为了这样做,你必须令所有直接继承自它的class采用virtual继承。
class File {...};
class InputFile : virtual public File {...};
class OutputFile : virtual public File {...};
class IOFile : public InputFile, public OutputFile
{...};
从正确的行为的观点看,public继承应该总是virtual。如果这是唯一一个观点,规则很简单:任何时候当你使用public继承,请改用virtual public继承。但是,正确性并不是唯一观点。为避免继承得来的成员变量重复,编译器必须提供若干幕后戏法,而其后果是:使用virtual继承那些class所产生的对象往往比使用non-virtual继承的兄弟们体积要大,访问virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢。virtual继承是需要代价的。
virtual继承的成本包括其他方面。支配“virtual base class”初始化的规则比起non-virtual base的情况远为复杂且不直观。virtual base的初始化责任是由继承体系的最底层 most deirved class负责--不论那些base距离多远,这暗示:(1)class若派生值virtual base而需要初始化,必须认知其virtual base--不论那些base距离多远,(2)当一个新的derived class加入继承体系中,它必须承担virtual base的初始化责任。
我对virtual base class的忠告很简答。第一,非必要不使用virtual base。平常请使用non-virtual继承。第二,如果你必须使用virtual base class,尽可能避免在其中放置数据。这样一来你就不需要担心这些class身上的初始化(和赋值)所带来诡异事情了。
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDay() const = 0;
};
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
// 这个函数从使用者手里取得一个数据库ID
DataBaseID askUserForDatabaseID();
DataBaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
IPerson的客户必须以IPerson的pointers或reference来编写程序,因为抽象class无法被实体化创建对象。
但是makePerson如何创建并返回一个指针指向它呢?无疑地一定是一定有某些派生自IPerson的具象class。我们假设其是CPerson。就像局限class一样,CPerson必须提供继承自IPerson的pure virtual函数的实现代码。假设有个既有的数据库相关class,名为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;
virtual const char* valueDelimClose() const;
};
const char* PersonInfo::valueDelimOpen() const
{
return "[";
}
const char* PersonInfo::valueDelimClose() const
{
return "]";
}
const char* PersonInfo::theName() const
{
static char value[Max-Formatted_Field_Value_Length];
std::strcpy(vallue, valueDelimOpen());
std::strcat(value, valueDelimClose());
return value;
}
CPerson和PersonInfo的关系是,PersonInfo刚好有若干函数可帮助CPerson轻松实现出来。
但CPerson也必须实现IPerson接口,那需以public继承才能完成。这导致多重继承的一个通情达理的应用:将“”public继承自某个接口”和”private继承自某实现”结合在一起。
UML图中这个设计是:
class CPerson : public IPerson, private PersonInfo
{
public:
explicit CPerson(DatabaseID pid) : PersonInfo(pid) {}
virtual std::string name() const
{ return PersonInfo::theName(); }
virtual std::string birthDay() const
{ return PersonInfo::theBirthDate(); }
private:
const char* valueDelimOpen() const { return ""; }
const char* valueDelimClose() ocnst { return ""; }
};
然而多重继承有时候的确是完成任务值最简洁、最易维护、最合理的做法。
请记住:
多重继承比单一继承复杂。它可能会导致新的歧义性,以及对virtual继承的需要。
virtual继承会增加大小、速度、初始化或赋值复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
多重继承的确有正当用途。其中一个情节涉及“public继承某个”Interface class和“private继承某个协助实现class”两相组合。