C箴言:接口继承和实现继承

C箴言:接口继承和实现继承,第1张

C箴言:接口继承和实现继承,第2张

表面上简单易懂的(公共)继承概念,将被证明由两个独立的部分组成:函数接口的继承和函数实现的继承。这两种继承之间的区别与本书导言中讨论的函数声明和函数定义之间的区别完全一致。

作为类设计者,有时您希望派生类只继承成员函数的接口(声明)。有时您希望派生类继承接口和实现,但是您必须允许它们替换它们继承的实现。有时您希望派生类继承一个函数的接口和实现,而不允许它们替换任何东西。

为了更好地感受这些选择之间的差异,考虑一个在图形应用程序中表示几何图形的类层次结构(类继承系统):

class Shape {
public:
virtual void draw()const = 0;

虚拟void错误(const STD::string & msg);

int objectID()const;

...
};

类矩形:公共形状{...};

类椭圆:公共形状{...};
Shape是一个抽象类,它的纯虚函数就说明了这一点。因此,客户不能创建Shape类的实例,而只能创建从它继承的类的实例。但是,Shape对从它继承的所有类(public)施加了非常强大的影响,因为

函数接口总是被继承。正如第32项所解释的,公共继承意味着是-a,所以对基类成立的东西对它的派生类也一定成立。因此,如果一个函数适用于一个类,它也必须适用于它的派生类。

Shape类中声明了三个函数第一个函数draw在显式显示设备上绘制当前对象。第二,错误,如果成员函数需要报告错误,就调用它。第三个objectID返回当前对象的整数标识符。每个函数的声明方式不同:draw是纯虚函数;错误是简单的(不纯的?)虚函数(简单虚函数);objectID是非虚函数。这些不同的说法暗示了什么?

考虑第一个纯虚函数draw:

class Shape {
public:
virtual void draw()const = 0;
...
};
纯虚函数的两个最显著的特点是,它们必须由继承它们的任何具体类重新声明,而且抽象类中一般没有对它们的定义。把这两个特征加在一起,你应该会意识到。

声明纯虚函数的目的是让派生类只继承一个函数接口。

这就使得Shape::draw函数完整了,因为合理的是所有的Shape对象都必须能够被绘制,但是Shape类本身并不能为这个函数提供一个合理的默认实现。比如画椭圆的算法和画矩形的算法就很不一样。Shape::draw的语句告诉具体派生类的设计者:“你必须提供一个draw函数,但我不给出你如何实现它的意见。”

顺便说一下,有可能提供一个纯虚函数的定义。也就是说,你可以为Shape::draw提供一个实现,C不会抱怨什么,但是调用它的方式是用类名限定这个调用:

Shape *ps =新形状;//错误!形状是抽象的

Shape *ps1 =新矩形;//fine
PS1-> draw();//调用Rectangle::draw

Shape *ps2 =新椭圆;//fine
PS2-> draw();//调用Ellipse::draw

PS1-> Shape::draw();//调用Shape::draw

PS2-> Shape::draw();//调用Shape::draw
除了在鸡尾酒会上帮你打动同行程序员,这个特性通常没什么用。然而,正如您将在下面看到的,它可以被用作一种机制来“为简单(脉冲)虚函数提供比通常更安全的实现”。

简单函数背后的故事与纯粹的虚拟函数有点不同。派生类仍然像往常一样是继承函数的接口,但是简单的虚函数提供了一个可以被派生类替换的实现。如果你想一会儿,你就会意识到

声明一个简单虚函数的目的是让派生类继承一个函数接口和一个默认实现。

考虑形状的情况::错误:

类形状{
public:
虚拟void错误(const STD::string & msg);
...
};
接口要求每个类都必须支持遇到错误时调用的函数,但是每个类都可以自由地以它认为合适的任何方式处理错误。如果一个类不需要做任何特殊的事情,它可以求助于Shape类中提供的错误处理的默认版本。也就是Shape::error的语句告诉派生类的设计者:“你应该支持一个错误函数,但是如果你不想自己写,可以求助于Shape类中的默认版本。”

因此,允许简单的虚函数同时指定函数接口和默认实现是很危险的。我们来看看为什么。考虑XYZ航空公司空的飞机的层次结构(继承系统)。XYZ飞机只有两种,A型和B型,都严格按照同一种方法飞行。所以XYZ是这样设计的:等级制(继承制):

机场等级{...};//代表机场

class plane {
public:
虚拟无效飞行(const Airport & destination);

...

};

无效飞机::飞行(const Airport&a

MP;目的地)
{
将飞机飞往给定目的地的默认代码
}

型号a级:公共飞机{...};

b类模型:公共飞机{...};

为了表达所有飞机都必须支持一个fly函数,也为了“不同的飞机模型可能(理论上)需要不同的fly实现”这一事实,将plane::fly声明为virtual。但是,为了避免ModelA和ModelB类中出现一些重复的代码,默认的飞行行为是由plane::fly的函数体提供的,由ModelA和ModelB继承。

这是一个经典的面向对象设计。因为两个类共享一个公共特性(它们实现fly方法),所以这个公共特性被转移到一个基类,并且这两个类继承这个特性。这种设计明确了共同的特征,避免了代码重复,增强了未来的可伸缩性,简化了长期维护——所有这些都因为面向对象技术而备受追捧。XYZ航空空应该为此感到骄傲。

现在,假设XYZ公司的财富增加了,它决定引入一个新的模型,模型C。模型C在某些方面不同于模型A和模型B。尤其是它的飞行方式与众不同。

XYZ公司的程序员在层次结构(继承系统)中加入了Model C的类,但是因为匆忙将新模型投入使用,他们忘记了重新定义fly函数:

类别型号c:公共飞机{

...//没有声明fly函数
};

位律师回复
DABAN RP主题是一个优秀的主题,极致后台体验,无插件,集成会员系统
白度搜_经验知识百科全书 » C箴言:接口继承和实现继承

0条评论

发表评论

提供最优质的资源集合

立即查看 了解详情