public继承概念由两部分组成:函数接口(function interface)继承和函数实现(function implementation)继承。
先来看个例子,考虑一个展现绘图程序中常见各种几何形状的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 class的实体,只能创建其derived class的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的derived class,因为:
成员函数的接口总是会被继承。public继承意味is-a,所以对base class为真的任何事情一定也对其derived class为真。因此,若某个函数可施行于某class身上,一定也可施行于其derived class身上。
Shape class声明了三个函数。
第一个是draw,于某个隐喻的视屏中画出当前对象。第二个是error,准备让那些“需要报导某个错误”的成员函数调用。第三个是objectID,返回当前对象的一个独一无二的整数识别码。每个函数的声明方式都不同:draw是个pure virtual函数,error是个简朴的(非纯)impure virtual函数,objectID是个non-virtual函数。
那么,这些不同的声明带来什么样的暗示呢?
首先考虑pure virtual函数draw:
class Shape{
public:
virtual void draw() const=0;
//...
};
pure virtual函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义。
把这两个性质摆在一起,你就会明白:
1.声明一个pure virtual函数的目的是为了让derived class只继承函数接口。
这对Shape::draw函数是再合理不过了,因为所有Shape对象都应该是可绘出的,这是合理的要求。但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法迥异于矩形绘法。Shape::draw的声明式乃是对具象derived class设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”
但令人意外的是,我们可以为pure virtual函数提供定义。也就是说你可以为Shape::draw供应一份实现代码,C++并不会发出怨言,但调用它的唯一途径是“调用时明确指出其class名称”:
class Rectangle: public Shape{/*...*/ };
class Ellipse: public Shape{/*...*/ };
Shape* ps = new Shape;//错误,Shape是抽象类
Shape* ps1 = new Rectangle;//没问题
ps1->draw();//调用Rectangle::draw()
Shape* ps2 = new Ellipse;//没问题
ps2->draw();//调用Ellipse::draw()
ps1->Shape::draw();//调用Shape::draw
ps2->Shape::draw();//调用Shape::draw
一般而言,这种性质用途有限,但它可以实现一种机制,为简朴的(非纯)impure virtual函数提供更平常更安全的缺省实现。
简朴的impure virtual函数背后的故事和pure virtual函数有点不同。一如往常,derived cass继承其函数接口,但impure virtual函数会提供一份实现代码,derived class可能覆写它。
声明简朴的(非纯)impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现。
考虑Shape::error这个例子:
class Shape{
public:
virtual void error(const std::string& msg);
//...
};
其接口表示,每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。
若某个class不想针对错误做出任何特殊行为,它可以退回到Shape class的设计者,“你必须支持一个error函数,但若你不想自己写一个,可以使用Shape class提供的缺省版本”。
但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。
欲探讨原因,先考虑XYZ航空公司设计的飞机继承体系。
该公司只有A型和B型两种飞机,两者都以相同方式飞行。因此XYZ设计出这样的继承体系:
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{/*...*/ };
为了表示所有飞机都一定能飞,并阐明“不同型飞机原则上需要不同的fly实现”,Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同代码,缺省飞行行为由Airplane::fly提供,它同时被ModelA和ModelB继承。
现在假设XYZ决定购买一种新式C型飞机。C型和A型以及B型有某些不同。更准确地说,它的飞行方式不同。
XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,竟忘了重新定义其fly函数:
class ModelC : public Airplane {
//...//未声明fly函数
};
然后在代码中有一些诸如此类的动作:
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)
{
//缺省代码,将飞机飞至指定的目的地
}
请注意,Airplane::fly已被改为一个pure virtual函数,只提供飞行接口。其缺省行为也出现在Airplane class中,但此次以独立函数defaultFly的姿态出现。若想使用缺省实现(例如ModelA和ModelB),可以在其fly函数中对defaultFly做一个inline调用:
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 class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:
class ModelC : public Airplane {
virtual void fly(const Airport& destination);
//...
};
void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定的目的地
}
但这个方案并非安全无虞,程序员还是可能因为剪贴代码而招来麻烦,但它的确比原先的设计值得倚赖。至于Airplane::defaultFly,请注意它现在成了protected,因为它是Airplane及其derived class的实现细目。
Airplane::defaultFly是个non-virtual函数,因为没有任何一个derived class应该重新定义此函数。若defaultFly是virtual函数,就会出现一个循环问题:万一某些derived class忘记重新定义defaultFly会怎样?
有些人反对以不同的函数分别提供接口和缺省实现,像上述的fly和defaultFly那样。他们关心因过度雷同的函数名称而引起的class命名空间污染问题。但他们也同意,接口和缺省实现应该分开。这个表面上看起来的矛盾该如何解决?
我们可以利用”pure virtual函数必须在derived class中重新声明,但它们也可以拥有自己的实现“这一事实。
下面便是Airplane继承体系如何给pure virtual函数一份定义:
class Airplane {
public:
virtual void fly(const Airport& destination)=0;
//...
};
void Airplane::fly(const Airport& destination)//pure virtual函数实现
{
//缺省代码,将飞机飞至指定的目的地
}
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 {
virtual void fly(const Airport& destination);
//...
};
void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定的目的地
}
这几乎和前一个设计一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割为两个基本要素:其声明部分表现的是接口(那是derived class必须使用的),其定义部分则表现出缺省行为(那是derived class可能使用的,但只有它们明确提出申请时才是)。若合并fly和defaultFly,就丧失了”让两个函数享有不同保护级别“的机会:习惯上被设为protectd的函数(defaultFly)如今成了public(因为它在fly之中)。
最后,让我们看看Shape的non-virtual函数objectID:
class Shape{
public:
int objectID() const;
//...
};
若成员函数是个non-virtual函数,意味它并不打算在derived class中有不同的行为。实际上一个non-virtual成员函数所表现的不变性(invariant)凌驾其特异性(specialization),因为它表示不论derived class变得多么特异化,它的行为都不可以改变。
就其自身而言:
声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。
pure virtual函数、simple(impure)virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived class继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。由于这些不同类型的声明意味着根本意义并不相同的事情,当你声明你的成员函数时,必须谨慎选择。若你确实履行,应该能够避免经验不足的class设计者最常犯的两个错误。
1.将所有函数声明为non-virtual。这使得derived class没有余裕空间进行特化工作。
non-virtual析构函数尤其会带来问题。当然,设计一个并不想成为base class的class是合理的,既然这样,将其所有成员函数都声明为non-virtual也很适当。但这种声明如果不是忽略了virtual和non-virtual函数之间的差异,就是过度担心virtual函数的效率成本。实际上任何class如果打算被用来当做一个base class,都会拥有若干virtual函数。
若你关心virtual函数的成本,你可以遵循80-20法则。这个法则说,一个典型的程序有80%的执行时间花费在20%的代码身上。
此一法则十分重要,因为它意味,平均而言你的函数调用中可以有80%是virtual而不冲击程序的大体效率。所以当你担心是否有能力负担virtual函数的成本之前,请先将心力放在那举足轻重的20%代码上,它才是真正的关键。
2.将所有成员函数声明为virtual。
1.接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口。
2.pure virtual函数只具体指定接口继承。
3.简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
4.non-virtual函数具体指定接口继承以及强制性实现继承。