区分接口继承和实现继承——条款34

        表面上直截了当的public继承概念,经过严密的检查之后,发现它由两部分组成:函数接口(function interfaces)继承函数实现(function implementations)继承。这两种继承的差异,很像本书导读所讨论的函数声明与函数定义之间的差异。
       身为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。所以客户不能够创建Shape class的实体,只能创建其derived classes的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的derived classes,因为:

  • 成员函数的接口总是会被继承。一如条款32所说,public继承意味is-a(是一种),所以对base class为真的任何事情一定也对其derived classes为真。因此如果某个函数可施行于某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中通常没有定义。把这两个性质摆在一起,你就会明白:

  • 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

        这对Shape::draw函数是再合理不过对事了,因为所有Shap对象都应该是可以绘出的,这是合理的要求。但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函数背后的故事和pure virtual函数有点不同。一如往常,derived classes继承其函数接口,但impure virtual函数会提供一份实现代码,derived classes可能覆写(override)它。稍加思索,你就会明白:

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。

        考虑Shape::error这个例子:

class Shape {
public:
	virtual void error(const std::string& msg);
	...
};

        Shape::error的声明式告诉derived classes的设计者,“你必须支持一个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继承。

        这是个典型的面向对象设计。两个classes共享一份相同性质(也就是它们实现fly的方式),所以共同性质被搬到base class中,然后被这两个classes继承。

        现在,假设XYZ盈余大增,决定购买一种新式C型飞机。C型和A型以及B型有某些不同。更明确地说,它的飞行方式不同。

         XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,竟忘了重新定义其fly函数:

class ModelC:public Airplane {
	...        // 未声明fly函数
};

        然后代码中有一些诸如此类的动作:

Airport PDX(...);               // 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 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调用(但请注意条款30所言,inline和virtual函数之间的交互关系):

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 {
	public:
	virtual void fly(const Airport& destination);
	...
};
void ModelC::fly(const Airport& destination)
{
	// 将C型飞机飞至指定目的地
}

        这几乎和前一个设计一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割为两个基本要素:其生命部分表现的是接口(那是derived classes必须使用的),其定义部分则表现出缺省行为(那是derived classes可能使用的,但只有它们明确提出申请时才是)。如果合并fly和defaultFly,就丧失了“让两个函数享有不同保护级别”的机会;习惯上被设为protected的函数(defaultFly)如今成了public(因为它在fly之中)。

        最后,让我们看看Shape的non-virtual函数objectID:

class Shape {
public:
	int objectID() const;
	...
};

        如果成员函数是个non-virtual函数,意味是它并不打算在derived classes中有不同的行为。实际上一个non-virtual成员函数的不变性凌驾其特异性,因为它表示不论derived class变得多么特异化,它的行为都不可以改变。就其自身而言:

  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

        pure virtual函数、impure virtual函数、non-virtual函数之间的差异,使你得以精确指定你想要derived classes继承的东西:只继承接口,或是继承接口和一份缺省实现,或是继承接口和一份强制实现。由于这些不同类型的声明意味根本意义不相同的事情,当你声明你的成员函数时,必须谨慎选择。如果你确实履行,应该能够避免经验不足的class设计者最常犯的两个错误。

        第一个错误是将所有函数声明为non-virtual。这使得derived classes没有余裕的空间进行特化工作。non-virtual析构函数尤其会带来问题(见条款7)。如果你关系virtual函数的成本,请容许我介绍所谓的80-20法则(也可见条款30)。这个法则说,一个典型的程序有80%的执行时间花费在20%的代码身上。此法则十分重要,因为它意味着,平均而言你的函数调用中可以有80%是vvirtual而不冲击程序的大体效率。所以当你担心是否有能力负担virtual函数的成本之前,请先将心力放在那举足轻重的20%代码上头,它才是真正的关键。

        第二个常见错误是将所有成员函数声明为virtual。有时候这样做是正确的,例如条款31的interface classes。然而这也可能是class设计者缺乏坚定立场的前兆。某些函数就是不该在derived class中被重新定义,果真如此你应该将那些函数声明为non-virtual。

请记住

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承即缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值