📄 9284.htm
字号:
<HTML>
<HEAD>
<meta http-equiv='Content-Type' content='text/html; charset=gb2312'>
<style >
.fst{padding:0px 15px;width:770px;border-left:0px solid #000000;border-right:0px solid #000000}
.fstdiv3 img{border:0px;border-right:8px solid #eeeecc;border-top:6px solid #eeeecc}
</style>
<title>
Effective C++ 2e Item36
</title>
</HEAD>
<BODY >
<center>
<div align=center><div class=fst align=left><div class=fstdiv3 id=print2>
<b>
Effective C++ 2e Item36
</b><P>条款36: 区分接口继承和实现继承</P>
<P>(公有)继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和函数实现的继承。这两种继承类型的区别和本书简介中所讨论的函数声明和函数定义间的区别是完全一致的。</P>
<P>作为类的设计者,有时希望派生类只继承成员函数的接口(声明);有时希望派生类同时继承函数的接口和实现,但允许派生类改写实现;有时则希望同时继承接口和实现,并且不允许派生类改写任何东西。</P>
<P>为了更好地体会这些选择间的区别,看下面这个类层次结构,它用来表示一个图形程序中的几何形状:</P>
<P>class Shape {<BR>public:<BR> virtual void draw() const = 0;</P>
<P> virtual void error(const string& msg);</P>
<P> int objectID() const;</P>
<P> ...</P>
<P>};</P>
<P>class Rectangle: public Shape { ... };</P>
<P>class Ellipse: public Shape { ... };</P>
<P>纯虚函数draw使得Shape成为一个抽象类。所以,用户不能创建Shape类的实例,只能创建它的派生类的实例。但是,从Shape(公有)继承而来的所有类都受到Shape的巨大影响,因为:</P>
<P>· 成员函数的接口总会被继承。正如条款35所说明的,公有继承的含义是 "是一个" ,所以对基类成立的所有事实也必须对派生类成立。因此,如果一个函数适用于某个类,也必将适用于它的子类。</P>
<P>Shape类中声明了三个函数。第一个函数,draw,在某一画面上绘制当前对象。第二个函数,error,被其它成员函数调用,用于报告出错信息。第三个函数,objectID,返回当前对象的一个唯一整数标识符(条款17给出了一个怎样使用这种函数的例子)。每个函数以不同的方式声明:draw是一个纯虚函数;error是一个简单的(非纯?)虚函数;objectID是一个非虚函数。这些不同的声明各有什么含义呢?</P>
<P>首先看纯虚函数draw。纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义。把这两个特征放在一起,就会认识到:</P>
<P>· 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。</P>
<P>这对Shape::draw函数来说非常有意义,因为,让所有Shape对象都可以被绘制是很合理,但Shape类无法为Shape::draw提供一个合理的缺省实现。例如,绘制椭园的算法就和绘制矩形的算法大不一样。打个比方来说,上面Shape::draw的声明就象是在告诉子类的设计者,"你必须提供一个draw函数,但我不知道你会怎样实现它。"</P>
<P>顺便说一句,为一个纯虚函数提供定义也是可能的。也就是说,你可以为Shape::draw提供实现,C++编译器也不会阻拦,但调用它的唯一方式是通过类名完整地指明是哪个调用:</P>
<P>Shape *ps = new Shape; // 错误! Shape是抽象的</P>
<P>Shape *ps1 = new Rectangle; // 正确<BR>ps1->draw(); // 调用Rectangle::draw</P>
<P>Shape *ps2 = new Ellipse; // 正确<BR>ps2->draw(); // 调用Ellipse::draw</P>
<P>ps1->Shape::draw(); // 调用Shape::draw</P>
<P>ps2->Shape::draw(); // 调用Shape::draw</P>
<P>一般来说,除了能让你在鸡尾酒会上给你的程序员同行留下深刻印象外,了解这种用法一般没大的作用。然而,正如后面将看到的,它可以被应用为一种机制,为简单的(非纯)虚函数提供 "比一般做法更安全" 的缺省实现。</P>
<P>有时,声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class),它为派生类仅提供函数接口,完全没有实现。协议类在条款34中介绍过,并将在条款43再次提及。</P>
<P>简单虚函数的情况和纯虚函数有点不一样。照例,派生类继承了函数的接口,但简单虚函数一般还提供了实现,派生类可以选择改写它们或不改写它们。思考片刻就可以认识到:</P>
<P>· 声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。</P>
<P>具体到Shape::error,这个接口是在说,每个类必须提供一个出错时可以被调用的函数,但每个类可以按它们认为合适的任何方式处理错误。如果某个类不想做什么特别的事,可以借助于Shape类中提供的缺省出错处理函数。也就是说,Shape::error的声明是在告诉子类的设计者,"你必须支持error函数,但如果你不想写自己的版本,可以借助Shape类中的缺省版本。"</P>
<P>实际上,为简单虚函数同时提供函数声明和缺省实现是很危险的。想知道为什么,看看XYZ航空公司的这个飞机类的层次结构。XYZ公司只有两种飞机,A型和B型,而且两种机型的飞行方式完全一样。所以,XYZ设计了这样的层次结构:</P>
<P>class Airport { ... }; // 表示飞机</P>
<P>class Airplane {<BR>public:<BR> virtual void fly(const Airport& destination);</P>
<P> ...</P>
<P>};</P>
<P>void Airplane::fly(const Airport& destination)<BR>{<BR> 飞机飞往某一目的地的缺省代码<BR>}</P>
<P>class ModelA: public Airplane { ... };</P>
<P>class ModelB: public Airplane { ... };</P>
<P>为了表明所有飞机都必须支持fly函数,而且因为不同型号的飞机原则上都需要对fly有不同的实现, 所以Airplane::fly被声明为virtual。但是,为了避免在ModelA类和ModelB类中写重复的代码,缺省的飞行行为是由Airplane::fly函数提供的,ModelA和ModelB继承了这一函数。</P>
<P>这是典型的面向对象设计。两个类享有共同的特征(实现fly的方式),所以这一共同特征被转移到基类,并让这两个类来继承这一特征。这种设计使得共性很清楚,避免了代码重复,将来容易增强功能,并易于长期维护 ---- 所有这一切正是面向对象技术高度吹捧的。XYZ公司真得为此而骄傲。</P>
<P>现在假设XYZ公司发了大财,决定引进一种新型飞机,C型。C型和A型、B型有区别,特别是,飞行方式不一样。</P>
<P>XYZ的程序员在上面的层次结构中为C型增加了一个类,但因为急于使新型飞机投入使用,他们忘了重新定义fly函数:</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -