本文系整理Effective C++中的条款34
首先当我们设计一个基类的时候,对于派生类的需求可能有下面几种
说这些晦涩难懂的文字,一时半会不好理解,下面分别举实例,用代码来解读这三种需求。
下面是一个展现绘图程序中各种几何图形的class继承体系:
class Shape
{
public:
//纯虚函数
virtual void draw()const = 0;
//普通的虚函数
virtual void error(const std::string& msg);
普通函数
int objectID()const;
...
};
class Rectangle:public Shape{...};
class Ellispse:public Shape{...};
这里首先说下,因为virtual void draw()const = 0;
是一个纯虚函数,从而使Shape类变为一个抽象类,即不能实例化出对象。
纯虚函数的声明:virtual + 函数返回值+函数名,在函数的后面紧跟=0。
这样就导致客户不能实例化出Shape的对象,只能实例化出其派生类的对象。但是基类还是影响了所有以public方式继承它的派生类,因为成员函数的接口总是被继承,public继承意味着is-a的关系。即保证任何一个对基类为真的事件,一定也对其派生类为真。
pure virtual(纯虚函数)的特性:
这就意味着声明一个pure virtual函数的目的是为了让派生类只继承函数的接口
但是令人意外的是,我们竟然可以为pure virtual函数提供定义,也就是说你可以为Shape::draw供应一份实现代码,c++并不会发出怨言,但是调用它的唯一途径是”调用时指明其class名称”
如下实例:
Shape* ps = new 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(普通虚函数)的特点
正如Shape::error这个例子:
class Shape
{
public:
virtual void error(const std::string& msg);
...
}
其接口表示,每个 class都必须支持一个“当遇上错误时可调用的函数”,但每个class可自由处理错误,如果你不想针对错做出任何特殊行为,则可以使用Shape class提供的缺省版本.
但是允许普通虚函数同时指定函数声明和函数缺省行为,有可能造成危险,考虑如下一种场景。
航空公司设计的飞机继承体系,该公司只有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{...};
典型的面向对象设计,两个classes共享一份相同性质的代码,这个共同的性质被搬到了基类中,然后被两个类同时继承,这个设计突显出共同性质,避免代码重复,减缓长期维护所需的成本,是个不错的模型吧?非也。
假设现在航空公司增加一种新型飞机C,并且C型飞机,并且飞行方式和A,B有所不同。
自然而然class ModelC:public Airpane{...}
,但是由于疏忽,并未从新定义fly函数,这时就采用了基类提供的缺省行为。后果不言而喻!!!!
问题不是Airplane::fly有缺省行为,而在于ModelC并未明白说出”我要”的情况下就继承了该缺省行为,幸运的是我们可以切断”virtual函数接口”和其”缺省实现”之间的联系。
看下面的代码实例
class Airplane
{
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
}
void Airplane::defaultFly(const Airport& destination)
{
//缺省行为,将飞机飞至指定的目的地
}
注意,上面的fly函数已经被声明为一个pure virtual函数,只提供飞行的接口,其缺省行为以一个保护的成员函数形式出现,子类若想使用缺省实现,可以在fly函数中对defaultFly做一个inline调用
class ModelA:public Airplane
{
public:
virtual void fly(const Airport& distination)
{
defaultFly(destination);
...
}
};
class ModelB:public Airplane
{
public:
virtual void fly(const Airport& distination)
{
defaultFly(destination);
...
}
};
现在ModelC class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual函数迫使ModelC必须提供自己的fly版本:
class ModelC:public Airplane
{
public:
virtual void fly(const Airport& destnation);
...
};
void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定的目的地
}
这里需要注意,Airplane::defaultFly现在成了protect的,因为它是Airplane及其derived classes的实现细节,乘客应该只在意飞机能不能飞,而不在意怎么飞的问题。
有些人反对不同的函数分别提供接口和缺省实现,就像上面的fly和defaultFly那样,他们关心因过度雷同的函数名称而引起class命名空间污染问题。但是它们也同意,接口和缺省应该分开,这个看似矛盾的问题该如何解决呢?可以利用”pure virtual函数必须在派生类中重新声明,但它们可以拥有自己的实现”这一事实。看下面最终的版本。
class ModelA:public Airplane
{
public:
virtual void fly(const Airport& distination) = 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
{
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
//将C型飞机飞至指定的目的地
}
这里的实现和前面几乎一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上现在的f’l’y被分割为两个基本要素:
如果合并fly和defaultFly,就丧失了”让两个函数享有不同保护级别”的机会,习惯上被定义为protected的函数(defaultFly)现在变成了public。
non-virtual(非虚函数)的特点
再来回到最开始的的继承体系Shape的non-virtual函数objectID
class Shape
{
int objectID()const;
...
};
该函数是一个non-virtual函数,意味着并不打算在派生类中有不同的行为,实际上一个non-virtual成员函数表现出来的不变性;凌驾于其特异性,因为无论派生类变得多么特异化,它的行为都不可以改变。
上面说的这么说,总结起来就是下面几点: