Effective C++读书笔记(22)

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

Differentiate between inheritance ofinterface and inheritance of implementation

public inheritance是由两个相互独立的部分组成的:inheritance of function interfaces(函数接口继承)和 inheritanceof function implementations(函数实现继承)。这两种 inheritance之间的差异很像函数声明和函数定义之间的差异。

作为类的设计者,有的时候你想要派生类只继承成员函数的接口(即声明);有的时候你想要派生类既继承接口也继承实现,但允许它们替换继承到的实现;还有的时候你想要派生类继承一个函数的接口和实现,而不允许替换。考虑在图形应用程序中表示几何图形的类继承体系:

class Shape {
public:
    virtual void draw() const = 0;
    virtual void error(conststd::string& msg);
    int objectID() const;
    ...
};

    classRectangle: public Shape { ... };
class Ellipse: public Shape { ... };

Shape 是一个抽象类,它的 pure virtual function函数表明了这一点。客户不能创建 Shape class 的实例,只能创建从它继承的类的实例。

l  成员函数的接口总是会被继承。publicinheritance 意味着 is-a,所以对基类成立的任何东西,对于它的派生类也成立。因此,如果一个函数适用于某类,它也一定适用于它的派生类。

Shape class 中声明了三个函数。第一个draw,在一个明确的显示设备上画出当前对象(pure virtual)。第二个error,如果成员函数需要报告一个错误就会调用它(impure virtual)。第三个objectID,返回当前对象的唯一整型标识符(non-virtual)。

首先考虑pure virtual函数draw:
virtual void draw() const= 0;

pure virtual函数有两个最显著的特性:1.它们必须被任何继承它们的具象类重新声明 和2.抽象类中一般没有它们的定义。

l  声明一个pure virtual函数的目的是使派生类只继承函数接口。

这对Shape::draw函数再合理不过,因为它要求所有的Shape对象必须是可绘制的,但是 Shape类本身不能为这个函数提供一个合乎情理的缺省实现。例如,画椭圆和画矩形的算法是非常不同的,Shape::draw的声明告诉派生类的设计者:“你必须提供一个draw函数,但我不干涉你怎么实现它。”

顺便提一句,我们还是可以调用Shape::draw,而C++也不会抱怨什么,但是调用它的唯一方法是明确指出类名称:

    Shape*ps = new Shape; // error! Shape is abstract
Shape *ps1 = new Rectangle; // fine
ps1->draw(); // calls Rectangle::draw
Shape *ps2 = new Ellipse; // fine
ps2->draw(); // calls Ellipse::draw
ps1->Shape::draw(); // calls Shape::draw
ps2->Shape::draw(); // calls Shape::draw

 

impure virtual函数和 pure virtual有一点不同。派生类照常还是继承函数的接口,但是 impurevirtual函数会提供一份实现代码,派生类可能override它。

l  声明impure virtual函数的目的是让派生类继承该函数的接口和缺省实现。

考虑 Shape::error 的情况:
virtual void error(conststd::string& msg);

其接口表示,每一个类必须支持一个在遭遇到错误时被调用的函数,但是每一个类可以自由处理错误。如果某个类不需要做什么特别的事情,它可以用Shape中错误处理的缺省版本。也就是说,Shape::error的声明告诉派生类的设计者:“你应该支持一个error函数,但如果你不想自己写,你可以用Shape中的缺省版本。”

结果是:允许impure virtual函数既指定函数接口又指定缺省实现是危险的。考虑XYZ航空公司的飞机继承体系。XYZ只有两种飞机,Model A和Model B,它们都按照同样的方法飞行:

    classAirport { ... };
class Airplane {
public:
    virtual void fly(const Airport&destination);
    ...
};

    voidAirplane::fly(const Airport& destination)
{缺省代码,将飞机飞至指定目的地}

    classModelA: public Airplane { ... };
class ModelB: public Airplane { ... };

为了表述不同机型理论上需要不同的对fly的实现,Airplane::fly被声明为virtual。然而,为了避免在ModelA和ModelB中重复的代码,缺省的飞行行为由 Airplane::fly的函数体提供,供ModelA 和 ModelB继承。

这是一个经典的面向对象设计。因为两个类共享一个通用特性(实现 fly的方法),所以这个通用特性被转移到基类之中,并由两个类来继承这个特性。现在假设XYZ公司决定引进一种新机型ModelC。ModelC的fly方式与 ModelA、ModelB 不同。特别是,它的飞行不同。

ModelC应该明确说出它要的fly方式,以避免不正确地继承基类中缺省的fly函数:

class Airplane {
public:
    virtual void fly(const Airport&destination) = 0;
    ...
protected:
    void defaultFly(const Airport&destination);
};
void Airplane::defaultFly(const Airport& destination)
{缺省行为,将飞机飞至指定目的地}

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); }
    ...
};

class ModelC: public Airplane {
public:
    virtual void fly(const Airport&destination);
    ...
};
void ModelC::fly(const Airport& destination)
{将C型飞机飞至指定目的地}

Airplane::defaultFly现在成了protected,因为它完全是Airplane和它的派生类的实现细节。Airplane::defaultFly 是一个 non-virtual函数)这一点也很重要。因为派生类不应该重定义这个函数。

 

一些人反对以不同的函数分别提供接口和缺省实现,就像上面的fly和defaultFly那样。然而他们仍然同意接口和缺省实现应该被分开。这个矛盾可以通过利用以下事实解决:purevirtual函数必须在派生类中重新声明,但是它们也可以有自己的实现:

class Airplane {
public:
    virtual void fly(const Airport&destination) = 0;
    ...
};
void Airplane::fly(constAirport& destination) // purevirtual函数实现
{ 缺省行为,将飞机飞至指定目的地 }

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型飞机飞至指定目的地}

除了用purevirtua纯虚拟函数Airplane::fly的函数体代替了独立函数 Airplane::defaultFly之外,这是一个和前面几乎完全相同的设计。本质上fly可以被拆成两个部分:它的声明指定了它的接口(这是派生类必须使用的),而它的定义指定它的缺省行为(这是派生类可以使用的,但只在他们明确要求时)。

最后,我们看看Shape的non-virtual函数objectID:
int objectID() const;

当成员函数是non-virtual,不应该指望它在派生类中的行为会有所不同。实际上,一个non-virtual成员函数所表现的不变性凌驾于特异性,因为不论一个=派生类)=变得多么特殊,它都把它看作是不允许变化的行为。=

l  声明non-virtual函数的目的是为了令派生类继承函数的接口及一份强制性实现。

你可以这样考虑Shape::objectID的声明:“每一个Shape对象有一个用来产生对象识别码的函数,且这个识别码总是用同样的方法计算出来的,这个方法是由Shape::objectID的定义决定的,任何派生类不应该试图改变它的做法。”

 

总结:

purevirtual:  只继承接口

impurevirtual:继承接口和一份缺省实现

non-virtual:  继承接口和一份强制实现

两个常犯错误:

1.    声明所有的函数为 non-virtual(非虚拟)。这没有给派生类的特殊化留出空间;当然,完全有理由设计一个不作为基类使用的类。事实是,几乎任何作为基类使用的类都会有virtual函数。

2.    声明所有的成员函数为virtual。某些函数在派生类中不应该被重定义,果真如此你应该通过将那些函数声明为non-virtual而明确地表达这一点。如果你的不变性凌驾于特异性,请直说!

 

  • 接口继承与实现继承不同。在 public继承下,派生类总是继承基类的接口。
  • Pure virtual函数只具体指定接口继承。
  • impure virtual函数具体指定接口继承加上缺省实现继承。
Non-virtual函数具体指定接口继承加上强制性实现继承。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值