📄 effective c++ 2e item35.htm
字号:
<TBODY>
<TR>
<TH align=left>Effective C++ 2e Item35
</TH></TD></TR></TBODY></TABLE>
<TABLE align=center bgColor=#eeeecc border=1 cellPadding=1 cellSpacing=0
width=770>
<TBODY>
<TR>
<TD align=left width=300><B>关键字:</B><BR>Effective C++ </TD>
<TD align=middle width=120><B>贴文时间</B><BR>2001-7-29 18:38:47 </TD>
<TD align=middle width=80><B>文章类型: </B><BR>翻译 </TD>
<TD align=middle width=100><B>给贴子投票 </B><BR><A
href="http://www.csdn.net/develop/addscore.asp?id=9254">投票</A> </TD></TR>
<TR>
<TD> lostmouse 翻译 </TD>
<TD colSpan=3 vAlign=top><B>出处: </B><A
href="http://www.csdn.net/develop/article/9/Effective%20C++%202e%20e-book">Effective
C++ 2e e-book </A></TD></TR>
<TR>
<TD bgColor=#cccc99 colSpan=5> </TD></TR></TD></TR></TBODY></TABLE>
<DIV align=center>
<DIV align=left class=fst>
<DIV class=fstdiv3 id=print2><BR><BR>
<P>继承和面向对象设计</P>
<P>很多人认为,继承是面向对象程序设计的全部。这个观点是否正确还有待争论,但本书其它章节的条款数量足以证明,在进行高效的C++程序设计时,还有更多的工具听你调遣,而不仅仅是简单地让一个类从另一个类继承。</P>
<P>然而,设计和实现类的层次结构与C语言中的一切都有着根本的不同。只有在继承和面向对象设计领域,你才最有可能从根本上重新思考软件系统构造的方法。另外,C++提供了多种很令人困惑的面向对象构造部件,包括公有、保护和私有基类;虚拟和非虚拟基类;虚拟和非虚拟成员函数。这些部件不仅互相之间有联系,还和C++的其它部分相互作用。所以,对于每种部件的含义、什么时候该用它们、怎样最好地和C++中非面向对象部分相结合
---- 要想真正理解这些,就要付出艰苦的努力。</P>
<P>使得事情更趋复杂的另一个原因是,C++中很多不同的部件或多或少地好象都在做相同的事。例如:</P>
<P>· 假如需要设计一组具有共同特征的类,是该使用继承使得所有的类都派生于一个共同的基类呢,还是使用模板使得它们都从一个共同的代码框架中产生?</P>
<P>· 类A的实现要用到类B,是让A拥有一个类型为B的数据成员呢,还是让A私有继承于B?</P>
<P>· 假设想设计一个标准库中没有提供的、类型安全的同族容器类(条款49列出了标准库实际提供的容器类),是使用模板呢,还是最好为某个
"自身用普通(void*)指针来实现" 的类建立类型安全的接口呢?</P>
<P>在本章节的条款中,我将指导大家怎样去回答这类问题。当然,我不可能顾及到面向对象设计的方方面面。相反,我将集中解释的是:C++中不同的部件其真正含义是什么,当使用某个部件时你真正做了什么。例如,公有继承意味着
"是一个" (详见条款35),如果使它成为别的什么意思,就会带来麻烦。相似地,虚函数的含义是 "接口必须被继承",非虚函数的含义是
"接口和实现都要被继承"。不能区分它们之间的含义会给C++程序员带来无尽的痛苦。</P>
<P>如果能理解C++各种部件的含义,你将发现自己对面向对象设计的认识大大转变。你将不再停留在为区分C++语言提供的不同部件而苦恼,而是在思考要为你的软件系统做些什么。一旦知道自己想做什么,将它转化为相应的C++部件将是一件很容易的事。</P>
<P>做你想做的,理解你所做的!这两点的重要性绝没有过分抬高。接下来的条款将对如何高效地实现这两点进行了详细的讨论。条款44总结了C++面向对象构造部件间的对应关系和它们的含义。它是本章节最好的总结,也可作为将来使用的简明参考。</P>
<P><BR>条款35: 使公有继承体现 "是一个" 的含义</P>
<P>在"Some Must Watch While Some Must Sleep"( W. H. Freeman and Company,
1974)一书中,William
Dement讲了一个故事,故事说的是他如何让学生们记住他的课程中最重要的部分。"据说,",他告诉他的学生,"一般的英国学生除了记得Hastings战役发生在1066年外,再也不记得其它历史。",
"如果一个小孩不记得别的历史," Dement强调说,"也一定记得1066这个日子。"
但对于他班上的学生来说,只有很少一些话题可以引起他们的兴趣,比如,安眠药会引起失眠之类。所以他哀求他的学生,即使忘掉他在课堂上讲授的其它任何东西,也要记住那些仅有的几个重要的历史事件。而且,他在整个学期不停地对学生灌输这一基本观点。</P>
<P>学期结束时,期末考试的最后一道题是,"请写下你从课程中学到的一辈子都会记住的东西"。当Dement评改试卷时,他大吃一惊。几乎所有学生都写下了
"1066"。</P>
<P>所以,在这里我也以极度颤抖的声音告诉你,C++面向对象编程中一条重要的规则是:公有继承意味着 "是一个" 。一定要牢牢记住这条规则。</P>
<P>当写下类D("Derived"
)从类B("Base")公有继承时,你实际上是在告诉编译器(以及读这段代码的人):类型D的每一个对象也是类型B的一个对象,但反之不成立;你是在说:B表示一个比D更广泛的概念,D表示一个比B更特定概念;你是在声明:任何可以使用类型B的对象的地方,类型D的对象也可以使用,因为每个类型D的对象是一个类型B的对象。相反,如果需要一个类型D的对象,类型B的对象就不行:每个D
"是一个" B, 但反之不成立。</P>
<P>C++采用了公有继承的上述解释。看这个例子:</P>
<P>class Person { ... };</P>
<P>class Student: public Person { ... };</P>
<P>从日常经验中我们知道,每个学生是人,但并非每个人是学生。这正是上面的层次结构所声明的。我们希望,任何对 "人" 成立的事实 ---- 如都有生日
----也对 "学生" 成立;但我们不希望,任何对 "学生" 成立的事实 ---- 如都在某一学校上学 ----也对 "人"
成立。人的概念比学生的概念更广泛;学生是一种特定类型的人。</P>
<P>在C++世界中,任何一个其参数为Person类型的函数(或Person的指针或Person的引用)可以实际取一个Student对象(或Student的指针或Student的引用):</P>
<P>void dance(const Person& p); //
任何人可以跳舞</P>
<P>void study(const Student& s); //
只有学生才学习</P>
<P>Person
p;
// p是一个人<BR>Student
s;
// s是一个学生</P>
<P>dance(p);
// 正确,p是一个人</P>
<P>dance(s);
//
正确,s是一个学生,<BR>
// 一个学生"是一个"人</P>
<P>study(s);
// 正确</P>
<P>study(p);
// 错误! p不是学生</P>
<P>只是公有继承才会这样。也就是说,只是Student公有继承于Person时,C++的行为才会象我所描述的那样。私有继承则是完全另外一回事(见条款42),至于保护继承,好象没有人知道它是什么含义。另外,Student
"是一个" Person的事实并不说明Student的数组 "是一个" Person数组。关于这一话题的讨论参见条款M3。</P>
<P>公有继承和 "是一个"
的等价关系听起来简单,但在实际应用中,可能不会总是那么直观。有时直觉会误导你。例如,有这样一个事实:企鹅是鸟;还有这样一个事实:鸟会飞。如果想简单地在C++中表达这些事实,我们会这样做:</P>
<P>class Bird {<BR>public:<BR> virtual void
fly();
// 鸟会飞</P>
<P> ...</P>
<P>};</P>
<P>class Penguin:public Bird { // 企鹅是鸟</P>
<P> ...</P>
<P>};</P>
<P>突然间我们陷入困惑,因为这种层次关系意味着企鹅会飞,而我们知道这不是事实。发生什么了?</P>
<P>造成这种情况,是因为使用的语言(汉语)不严密。说鸟会飞,并不是说所有的鸟会飞,通常,只有那些有飞行能力的鸟才会飞。如果更精确一点,我们都知道,实际上有很多种不会飞的鸟,所以我们会提供下面这样的层次结构,它更好地反映了现实:</P>
<P>class Bird {<BR>
...
// 没有声明fly函数<BR>}; </P>
<P>class FlyingBird: public Bird {<BR>public:<BR> virtual void
fly();<BR> ...<BR>};</P>
<P>class NonFlyingBird: public Bird {</P>
<P>
...
// 没有声明fly函数<BR>};</P>
<P>class Penguin: public NonFlyingBird {</P>
<P>
...
// 没有声明fly函数<BR>};</P>
<P>这种层次就比最初的设计更忠于我们所知道的现实。</P>
<P>但关于鸟类问题的讨论,现在还不能完全结束。因为在有的软件系统中,说企鹅是鸟是完全合适的。比如说,如果程序只和鸟的嘴、翅膀有关系而不涉及到飞,最初的设计就很合适。这看起来可能很令人恼火,但它反映了一个简单的事实:没有任何一种设计可以理想到适用于任何软件。好的设计是和软件系统现在和将来所要完成的功能密不可分的(参见条款M32)。如果程序不涉及到飞,并且将来也不会,那么让Penguin派生于Bird就会是非常合理的设计。实际上,它会比那个区分会飞和不会飞的设计还要好,因为你的设计中不会用到这种区分。在设计层次中增加多余的类是一种很糟糕的设计,就象在类之间制定了错误的继承关系一样。</P>
<P>对于 "所有鸟都会飞,企鹅是鸟,企鹅不会飞"
这一问题,还可以考虑用另外一种方法来处理。也就是对penguin重新定义fly函数,使之产生一个运行时错误:</P>
<P>void error(const string& msg); // 在别处定义</P>
<P>class Penguin: public Bird {<BR>public:<BR> virtual void fly() {
error("Penguins can't fly!"); }</P>
<P> ...</P>
<P>};</P>
<P>解释型语言如Smalltalk喜欢采用这种方法,但这里要认识到的重要一点是,上面的代码所说的可能和你所想的是完全不同的两回事。它不是说,"企鹅不会飞",而是说,"企鹅会飞,但让它们飞是一种错误"。</P>
<P>怎么区分二者的不同?这可以从检测到错误发生的时间来区分。"企鹅不会飞" 的指令是由编译器发出的,"让企鹅飞是一种错误" 只能在运行时检测到。</P>
<P>为了表示 "企鹅不会飞" 这一事实,就不要在Penguin对象中定义fly函数:</P>
<P>class Bird {</P>
<P>
...
//
没有声明fly函数<BR>
<BR>};</P>
<P>class NonFlyingBird: public Bird {</P>
<P>
...
//
没有声明fly函数<BR>
<BR>};</P>
<P>class Penguin: public NonFlyingBird {</P>
<P>
...
//
没有声明fly函数<BR>
<BR>};</P>
<P>如果想使企鹅飞,编译器就会谴责你的违规行为:</P>
<P>Penguin p;</P>
<P>p.fly();
// 错误!</P>
<P>用Smalltalk的方法得到的行为和这完全不同。用那种方法,编译器连半句话都不会说。</P>
<P>C++的处理方法和Smalltalk的处理方法有着根本的不同,所以只要是在用C++编程,就要采用C++的方法做事。另外,在编译时检测错误比在运行时检测错误有某些技术上的优点,详见条款46。</P>
<P>也许你会说,你在鸟类方面的知识很贫乏。但你可以借助于你的初等几何知识,对不对?我是说,矩形和正方形总该不复杂吧?</P>
<P>那好,回答这个简单问题:类Square(正方形)可以从类Rectangle(矩形)公有继承吗?</P>
<P>
Rectangle<BR>
^<BR>
| ?<BR> Square</P>
<P>"当然可以!" 你会不屑地说,"每个人都知道一个正方形是一个矩形,但反过来通常不成立。"
确实如此,至少在高中时可以这样认为。但我不认为我们还是高中生。</P>
<P>看看下面的代码:</P>
<P>class Rectangle {<BR>public:<BR> virtual void setHeight(int
newHeight);<BR> virtual void setWidth(int newWidth);</P>
<P> virtual int height()
const; // 返回当前值<BR>
virtual int width()
const; // 返回当前值</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -