public继承有两部分组成:
身为类的设计者,有时候你会希望派生类只继承成员函数的接口(也就是声明);有时候你会希望派生类同时继承函数的接口和实现,有时候你会希望能够覆写(override)它们所继承的实现;有时候你会希望生类同时继承函数的接口和实现,并且不允许覆写任何东西。
我们来看个例子:
class Shape{
public:
virtual void draw() const = 0;
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangel : public Shape {...}
class Ellipse : public Shape {...}
Shape是个抽象类:它的纯虚函数draw使它成为一个抽象类。所以客户不能够创建Shape类的实体,只能创建其派生类的实体。 尽管如此,Shape还是强烈影响了所有以public形式继承它的派生类,因为:成员函数的接口总是会被继承。
virtual void draw() const = 0
。
继承了它们
的具现类重新声明Shape* ps = new Shape; // 错误,Shape是抽象的
Shape* ps1 = new Rectangle; //没问题
ps1->draw(); // 调用Rectangle::draw
ps1->Shape::draw(); // 调用Shape::draw
一般而言,这项性质用途有限,但是稍后你将看到,它可以实现一种机制,为简朴的impure virtual函数提供更平常更安全的缺省实现。
简朴的impure virtual函数和pure virtual函数有点不同。一如往常,派生类继承其函数接口,但非纯虚函数会提供一份实现代码,派生类可能覆写(overried)它,也就是说:
看个例子:
class Shape{
public:
virtual void error(const std::string& msg);
}
其error接口表示,每个类都必须支持一个”当遇上错误是可调用“的函数,但每个类可以自由处理错误,如果每个类不想针对错误做出任何特殊行为,它可以退回到Shape类提供的缺省错误处理行为。也就是说,Shape::error告诉派生类:”你可以一个error函数,用来处理错误,但如果你不想自己写一个,可以使用Shape class提供的缺省版本“。
但是,允许impure virtual函数同时指定函数声明和函数缺省行为,有可能造成危险。我们来看一个例子。
场景: 一家公司只有两种飞机,两者都以相同的方式飞行
实现:为了表现所有飞机都一定能飞,并阐明”不同型飞机原则上需要不同的fly实现“。Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中编写相同代码,缺省行为由Airplane::fly提供,它同时被ModelA和ModelB继承。如下
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 {}
新需求:加入这家公司又买了一种C型飞机,这种飞机的不是采用默认的飞行方式,但是不小心忘记了重新定义fly函数:
class ModelC : public Airplane {}
这将造成重大灾难。
问题不在Airplane::fly有缺省行为,而在于ModelC在没有明白说出"我要"的情况下就缺省继承了该缺省行为。幸运的是我们可以轻易做到"提供缺省实现给派生类,但除非它们要求否则免谈"。技巧在于切断”虚函数“和"缺省实现"之间的连接。下面是一种做法。
class Airplane{
public:
virtual void fly(const Ariport& destination) = 0;
protected:
void defaultFly(const Ariport& destination);
}
void Airplane::defaultFly(const Airport& destination){
// 缺省代码,将飞机飞往指定目的地
};
现在Airplane::fly已经被改为一个纯虚函数,只提供飞行接口。其缺省行为以独立的defaultFly函数提供。如果派生类想要使用缺省实现,可以在其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类意外继承不正确的fly实现了,因为Airplane的纯虚函数强迫ModelC必须提供自己的fly版本。
另外,注意
有人反对以不同的函数分别提供接口和缺省实现(fly,default),会应为过渡雷同的函数名称而引起类命名空间污染问题。但是他们也同意,接口和缺省应该分开。那这个矛盾怎能解决呢?我们可以利用纯虚函数必须在派生类中重新声明,但是它们也可以有自己的实现这一事实来解决:
class Airplane{
public:
virtual void fly(const Ariport& destination) = 0;
};
void Airplane::fly(const Ariport& 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);
}
}
本质上,现在的fly被分成两个基本要素:其声明部分表现的是接口(派生类必须用的),其定义部分标注的是缺省行为(派生类可能要用的,但只有它们明确提出申请才是)。
最后,我们来看看Shape的non-virtual的objectID:
class Shape{
public:
int objectID() const;
};
如果成员函数是个non-virtual函数,意味着它并不打算在派生类中有不同的行为。实际上,一个non-virtual成员函数所表现的不变性凌驾于其特异性,因为它表示不论派生类变得多么特异化,它的行为都不可以改变。就自身而言:
Shape::objectID的声明相当于:”每个Shape对象都有一个用来产生对象识别码的函数,此识别码总是采用相同的计算方法,该方法由Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为“。
纯虚函数,impure virtual函数、non-virtual函数之间的差异,使得你得以精确指定你想要派生类继承的东西:只继承接口,或者是继承接口和一份缺省实现,或者是继承接口和一份强制实现。由于这些不同类型的声明意味着根本意义并不相同的事情,当你声明你的成员函数,必须谨慎选择。注意
不要将所有的函数都声明为non-vritual,这使得派生类没有空间进行特化工作。non-vritual析构函数绝对会带来问题。事实上:
不要将所有的成员函数都声明为virtual