总结:
1. 接口继承&实现继承不同。在public继承之下,derived classes总是继承base class的接口。
2. pure virtual纯虚函数只具体指定接口继承。
3. Impure virtual普通虚函数具体指定接口继承及缺省实现继承。
4. non-virtual普通函数具体指定接口继承以及强制性实现继承。
公有继承的概念看似简单,似乎很轻易就浮出水面,然而仔细审度之后,我们会发现公有继承的概念实际上包含两个相互独立的部分:函数接口的继承和函数实现的继承。二者之间的差别恰与函数声明和函数实现之间相异之处等价。成员函数的接口总是被继承,因为public继承意味is-a(文章:条款32 确定你的public继承塑模出is-a关系)。
class Shape {
public:
virtual void draw() const = 0; //纯虚函数
virtual void error(const string& msg); //普通虚函数
int objectID() const; //普通函数
};
class Rectangle : public Shape {...};
class Ellipse : public Shape {...};
一、纯虚函数
pure virtual 函数两个突出特征:(1)必须被任何“继承了它们”的具象class重新声明;(2)它们在抽象class中通常没有定义。
声明一个pure virtual函数的目的是为了让derived class只继承函数接口(纯虚函数draw的声明是对其派生类设计者说:你必须提供draw函数,但是我不干涉你如何实现它)。这对draw函数的设计是合理的:任何Shape对象都应该是可以绘出来的,但是Shape类无法为该函数提供合理的缺省实现,毕竟椭圆的绘制法和矩形绘制法迥异。
令人意外的是,竟然可以为纯虚函数提供定义,也就是为它提供一份实现代码,C++对它并没有任何怨言,但调用它的唯一途径是“调用时明确指出期class的名称”:
Shap<span style="font-family:宋体;">e *ps = new Shape; <span style="line-height:28px; font-family:宋体"><span style="background-color:rgb(255,255,255)"></span></span></span>//错误,Shape是抽象类,不能<span style="font-family:宋体;">对</span>抽象类进行实例化<span style="font-family:宋体;"><span style="line-height:28px; font-family:宋体"><span style="background-color:rgb(255,255,255)"></span></span></span>Sharp* ps1 = new Rectangle;
ps1->draw();
Sharp* ps2 = new Rectangle;
ps2->draw();
ps1->Shape::draw(); //调用Shape的draw
ps2->Shape::draw();
这样对纯虚函数的实现用途有限;但是一如稍后你将看到,它可以实现一种机制:为普通虚函数提供更平常更安全的缺省实现。
二、普通虚函数
例如上面Shape类的普通虚函数error();
声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现(普通虚函数是告诉派生类的设计者:你必须支持一个error函数,但如果你不想自己写一个,可以使用基类Shape提供的缺省版本)。
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 {...};
class ModelC : public Airplane {
...//未声明fly函数,但它并不希望缺省飞行
};
Airport PDX(...); //PDX是我家附近的机场
Airplane* pa = new ModelC;
...
pa->fly(PDX); //未说出“我要的情况下就继承了该缺省行为,酿成大灾难”
A型和B型两种飞机,它们以相同方式飞行,因此有了上述的继承方式。基类Airplane::fly被声明为虚函数,为了避免A和B撰写相同的飞行代码,为Airplane::fly提供了缺省的飞行方式,这被A和B继承。但是新的C型飞机的飞行方式和A、B的飞行方式不一样,但是如上代码:忘记为C定义fly函数,这将是灾难。
解决方法1
上述问题不在Airplane::fly有缺省行为,而在于C类在未明确说明要继承该缺省行为的情况下却继承了该行为。幸运的是可以轻易通过“只有派生类明确说明要用基类缺省行为才为派生类提供缺省实现,否则不为派生类提供缺省行为”。此间伎俩在于切断“虚函数接口”和“缺省实现”之间的连接:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0; //注意,fly被声明成了虚函数,仅提供飞行接口
...
protected:
void defaultFly(const Airport& destination); //这是fly的缺省行为,被用独立的函数呈现
};
void Airplane::defaultFly(const Airport& destination) {
//缺省行为,将飞机飞至目的地
}
现在想要使用缺省实现(A类和B类),可以在其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);
}
...
};
注意,defaultFly是Protected类型,因为它是Airplane以及其派生类的实现细节;乘客应该只关心飞机能不能飞,而不在意飞机怎么飞。Airplane::defaultFly是非虚函数,这很重要:因为没有任何一个派生类应该重新定义该函数。现在ModelC不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination) {
//将C型飞机飞至指定的目的地
}
解决方法2
有些人反对以不同的函数分别提供接口和缺省实现,向上述的fly和defaultFly那样。他们担心过度雷同的函数名称会引起类命名空间污染问题。但是他们也同意,接口和缺省实现应该分开。方法:利用“纯虚函数必须在派生类中重新声明,但是它们可以用于自己的实现”这一事实。下面是Airplane继承体系如何给pure virtual函数一份定义:
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 ModelC::fly(const Airport& destination) {
// 将C型飞机飞至指定目的地
}
这个实现和前一个类似,只是用纯虚函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割成两个基本要素:其声明部分的是接口(那个派生类必须使用的),其定义部分则表现出缺省行为(那是派生类可能使用的,但是只有在它们明确提出申请时才会)。三、非虚函数
看Shape的非虚函数objectID:
class Shape {
public:
...
int objectID() const;
};
如果成员函数是个non-virtual函数,意味着它并不打算在derived classes中有不同的行为。non-virtual 成员函数所表现的
不变性凌驾其特异性
,无论derived class变得多么特异化,它的行为都不可以改变,这是条款36讨论的重点。
声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。
来看Shape::objectID的声明:可以想做是“每个Shape对象都有一个用来产生对象识别码的函数:此识别码总是采用相同计算方法,该方法由Shape::objectID的定义式决定,任何derived class都不应该尝试改变其行为”。
四、两个常见错误
1、将所有函数声明为非虚函数
这使得派生类没有多余的空间进行特化工作。非虚析构函数会带来问题(参考文章条款07 为多态基类声明virtual析构函数)。当然,设计一个并不想成为基类的类就不要有虚函数了。但将所有函数声明为非虚函数是因为担心虚函数带来的效率成本,那么这种声明就是错误的。实际上,任何类如果打算作为虚基类,就会拥有若干个虚函数(条款07 为多态基类声明virtual析构函数)。
如果你关心虚函数成本,请注意80-20法则:一个典型的程序80%的执行时间会花费在20%的代码身上。这一法则很重要,因为它意味着,平均而言你的函数调用中可以有80%是虚函数而不会冲击程序的大体效率。所以当担心virtual函数的成本前,先将心力放在那举足轻重的20%代码上,它才是关键。
2、将所有函数声明为虚函数
有时候这样是正确的,例如条款31的interface classes。然而这也可能是类设计者缺乏坚定立场的前兆。
某些函数就是不该在派生类中重新定义的,那么就应该将其设计为非虚函数,毕竟虚函数有额外成本。