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

1.前言

表面上是public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。这两种继承的差异,很像本书所讨论的函数声明和函数定义之间的差异。

作为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。所以客户不能够创建Shapen class的实体,只能够创建derived classes的实体。尽管如此,Shape还是强烈影响了以public形式继承它的derived classes,因为:

成员函数的接口总是会被继承,public继承意味着is-a,所以对base class为真的事情一定也对其derived classes为真,因此某个函数可施行于某class身上,一定也可以施行于derived classes身上。

Shape class声明了三个函数,第一个是draw,第二个是error,第三个是objectID,返回当前对象的一个独一无二的整数识别码。每个函数的声明方式都不一样:draw是个pure virtual函数;error是个impure virtual函数;objectID是个non-virtual函数。

2.实例分析

首先考虑pure virtual函数draw:

class Shape{

    public:
        virtual void draw(0 const=0;
        ...
};

该函数有两个最突出的特性:它们必须被任何“继承了它们”的具象class重新声明,而且它们在抽象class中通常没有定义,这都表明声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。

因为所有Shape对象都应该是可以绘出的,但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函数提供更平常更安全的缺省实现。

impure virtual函数和pure virtual函数有点不同,derived  classes继承其函数接口,但impure virtual函数会提供一份实现代码,dereived classes会override它。由此可知impure virtual函数的目的是让derived classes继承该函数的接口和缺省实现。

见以下这个例子:

class Shape{

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

其接口表示为每个class都要支持一个“当遇上错误时可调用”的函数,但每个class可自由处理错误。如果某个class不想针对错误做出任何特殊行为,它可以退回到Shape class提供的缺省错误处理行为。也就是说Shape::error的声明式告诉derived classes的设计者,必须支持一个error函数。倘若不想自己重写,也可以使用Shape class提供的缺省版本

但是,允许impure virtual函数同时指定函数声明和函数缺省行为,也有可能造成危险。以以下例子来讲:某公司有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共享一份相同性质,所以共同性质被搬到base class中,然后被两个class继承。这个设计凸显出共同属性,避免代码的重复,减轻维护所需的成本。

现在假设有一架新式C型飞机,其的飞行方式不同。倘若程序员在继承体系中忘记了重新定义其fly函数:

class Modeel:public Airplane{

    ...
};

然后代码中有类似的动作:

Airplane 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 Airplane& destination)=0;
        ...
    protected:
        void defaultFly(const Airplane& destination);
};
void Airplane::defaultFly(const Airplane& destination)
{
    //缺省行为,将飞机飞至指定的目的地
}

这里需要注意,Airplane::fly已经被改为pure virtual函数,只提供飞行接口,其缺省行为也出现在Airplane class中,但此次是以独立函数defaultFly的姿态出现。若想使用缺省实现(比如ModelA和ModelB),可以在fly函数中对defaultFly做一个inline调用。见以下例子:

class ModelA:public Airplane{

    public:
        virtual void fly(const Airplane& destination)
        {
            defaultFly(destination);
        }
        ...

};
class ModelB:public Airplane{

    public:
        virtual void fly(const Airplane& destination)
        {
            defaultFly(destination);
        }
        ...
};

现在ModelC class不可能意外继承不正确的fly实现代码了,因为Airplane中的pure virtual迫使ModelC必须提供自己的fly版本:

class ModelC:public Airplane{

    public:
        virtual void fly(const Airplane& destination);
        ...
};

void ModelC::fly(const Airplane& destination)
{

    //将c型飞机飞至指定的目的地
}

该方案也并非完全没有缺点,但它确实比原先的设计更值得依赖,至于Airplane::defaultFly,请注意它现在成了protected,因为它是Airplane及其derived classes的实现细目,乘客应该在意该飞机能不能飞,而不是怎么飞。

Airplane::defaultFly是个non-virtual函数,因为没有任何一个derived classes应该重新定义此函数。

3.总结

综合以上内容,总结为以下几点:

1.接口继承和实现继承不同,在public继承之下,derived classes总是继承base class的接口

2.pure virtual函数只具体指定接口继承

3.impure virtual函数具体指定接口继承及缺省实现继承

4.non-virtual函数具体指定接口继承以及强制性实现继承

相关推荐

  1. 34区分接口继承实现

    2024-01-08 16:06:01       54 阅读
  2. 《Effective C++》33

    2024-01-08 16:06:01       59 阅读
  3. 《Effective C++》37

    2024-01-08 16:06:01       57 阅读
  4. 41:了解隐式接口编译器多态

    2024-01-08 16:06:01       62 阅读
  5. effective c++ 笔记 26-31

    2024-01-08 16:06:01       42 阅读
  6. php中的继承接口

    2024-01-08 16:06:01       55 阅读
  7. Unity 中的接口继承

    2024-01-08 16:06:01       45 阅读
  8. python单继承继承实例讲解

    2024-01-08 16:06:01       26 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-01-08 16:06:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-01-08 16:06:01       101 阅读
  3. 在Django里面运行非项目文件

    2024-01-08 16:06:01       82 阅读
  4. Python语言-面向对象

    2024-01-08 16:06:01       91 阅读

热门阅读

  1. es相关介绍:yml配置、基础接口及方法介绍

    2024-01-08 16:06:01       50 阅读
  2. 尝试中-分3个独立开发周期

    2024-01-08 16:06:01       68 阅读
  3. axios 后端不配和添加api

    2024-01-08 16:06:01       64 阅读
  4. Intertek绿叶标志——产品碳足迹

    2024-01-08 16:06:01       69 阅读
  5. 「HDLBits题解」Vector2

    2024-01-08 16:06:01       65 阅读
  6. 学习记录————

    2024-01-08 16:06:01       66 阅读
  7. SpringCloud入门

    2024-01-08 16:06:01       59 阅读