表面上是public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。这两种继承的差异,很像本书所讨论的函数声明和函数定义之间的差异。
作为class的设计人员,有时候只希望derived classes只继承成员函数的接口(即声明),有时候又希望derived classes同时继承函数的接口和实现,但又希望能够覆盖(override)它们所继承的实现;又有时候希望derived classes同时继承函数的接口和实现,并且不允许覆盖任何东西。
为了更好的感觉上述选择的差异,以下是一个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。所以客户不能够创建Shapen class的实体,只能够创建derived classes的实体。尽管如此,Shape还是强烈影响了以public形式继承它的derived classes,因为:
成员函数的接口总是会被继承,public继承意味着is-a,所以对base class为真的事情一定也对其derived classes为真,因此某个函数可施行于某class身上,一定也可以施行于derived classes身上。
Shape class声明了三个函数,第一个是draw,第二个是error,第三个是objectID,返回当前对象的一个独一无二的整数识别码。每个函数的声明方式都不一样:draw是个pure virtual函数;error是个impure virtual函数;objectID是个non-virtual函数。
首先考虑pure virtual函数draw:
class Shape{
public:
virtual void draw(0 const=0;
...
};
该函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义,这都表明声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
因为所有Shape对象都应该是可以绘出的,但Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘法不同于矩形绘法。Shape::draw的声明式仍是对具象derived classes设计者说的,即你必须提供一个draw函数,但我不干涉其怎么实现它。
但同时,我们也可以为pure virtual函数提供定义,也就是说你可以为Shape::draw供应一份实现代码,c++并不会报错,但调用它的唯一途径是“调用时明确指出其class名称”。
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 classes继承其函数接口,但impure virtual函数会提供一份实现代码,dereived classes会override它。由此可知impure virtual函数的目的是让derived classes继承该函数的接口和缺省实现。
见以下这个例子:
class Shape{
public:
virtual void error(const std::string& msg);
...
};
其接口表示为每个class都要支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉derived classes的设计者,必须支持一个error函数。倘若不想自己重写,也可以使用Shape class提供的缺省版本。
但是,允许impure virtual函数同时指定函数声明和函数缺省行为,也有可能造成危险。以以下例子来讲:某公司有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继承。
这个典型的面向对象设计,两个classes共享一份相同性质,所以共同性质被搬到base class中,然后被两个class继承。这个设计凸显出共同属性,避免代码的重复,减轻维护所需的成本。
现在假设有一架新式C型飞机,其的飞行方式不同。倘若程序员在继承体系中忘记了重新定义其fly函数:
class Modeel:public Airplane{
...
};
然后代码中有类似的动作:
Airplane PDX(...);
Airplane* pa=new ModelC;
...
pa->fly(PDX);//调用Airplane::fly
这是个及其明显的错误:试图以ModelA或ModelB的飞行方式来飞ModelC。
问题不在于Airplane::fly有缺省行为,在于ModelC未知的情况下就继承了该缺省行为。幸运的是我们可以轻易做到”提供缺省实现给derived classes,但除非它们明确要求,否则免谈“。这里的关键是在于切断”virtual函数接口“和其”缺省实现“之间的连接,实现方法如下:
class Airplane{
public:
virtual void fly(const Airplane& destination)=0;
...
protected:
void defaultFly(const Airplane& destination);
};
void Airplane::defaultFly(const Airplane& destination)
{
//缺省行为,将飞机飞至指定的目的地
}
这里需要注意,Airplane::fly已经被改为pure virtual函数,只提供飞行接口,其缺省行为也出现在Airplane class中,但此次是以独立函数defaultFly的姿态出现。若想使用缺省实现(比如ModelA和ModelB),可以在fly函数中对defaultFly做一个inline调用。见以下例子:
class ModelA:public Airplane{
public:
virtual void fly(const Airplane& destination)
{
defaultFly(destination);
}
...
};
class ModelB:public Airplane{
public:
virtual void fly(const Airplane& destination)
{
defaultFly(destination);
}
...
};
现在ModelC class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual迫使ModelC必须提供自己的fly版本:
class ModelC:public Airplane{
public:
virtual void fly(const Airplane& destination);
...
};
void ModelC::fly(const Airplane& destination)
{
//将c型飞机飞至指定的目的地
}
该方案也并非完全没有缺点,但它确实比原先的设计更值得依赖,至于Airplane::defaultFly,请注意它现在成了protected,因为它是Airplane及其derived classes的实现细目,乘客应该在意该飞机能不能飞,而不是怎么飞。
Airplane::defaultFly是个non-virtual函数,因为没有任何一个derived classes应该重新定义此函数。
综合以上内容,总结为以下几点:
1.接口继承和实现继承不同,在public继承之下,derived classes总是继承base class的接口
2.pure virtual函数只具体指定接口继承
3.impure virtual函数具体指定接口继承及缺省实现继承
4.non-virtual函数具体指定接口继承以及强制性实现继承