📄 9254.htm
字号:
<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>
<P> ...</P>
<P>};</P>
<P>void makeBigger(Rectangle& r) // 增加r面积的函数<BR>{ <BR> int oldHeight = r.height();</P>
<P> r.setWidth(r.width() + 10); // 对r的宽度增加10</P>
<P> assert(r.height() == oldHeight); // 断言r的高度未变<BR>} </P>
<P>很明显,断言永远不会失败。makeBigger只是改变了r的宽度,高度从没被修改过。</P>
<P>现在看下面的代码,它采用了公有继承,使得正方形可以被当作矩形来处理:</P>
<P>class Square: public Rectangle { ... };</P>
<P>Square s;</P>
<P>...</P>
<P>assert(s.width() == s.height()); // 这对所有正方形都成立</P>
<P><BR>makeBigger(s); // 通过继承,s "是一个" 矩形<BR> // 所以可以增加它的面积<BR> <BR>assert(s.width() == s.height()); // 这还是对所有正方形成立</P>
<P>很明显,和前面的断言一样,后面的这个断言也永远不会失败。因为根据定义,正方形的宽和高应该相等。</P>
<P>那么现在有一个问题。我们怎么协调下面的断言呢?</P>
<P>· 调用makeBigger前,s的宽和高相等;<BR>· makeBigger内部,s的宽度被改变,高度未变;<BR>· 从makeBigger返回后,s的高度又和宽度相等。(注意s是通过引用传给makeBigger的,所以makeBigger修改了s本身,而不是s的拷贝)</P>
<P>怎么样?</P>
<P>欢迎加入公有继承的精彩世界,在这里,你在其它研究领域养成的直觉 ---- 包括数学 ---- 可能不象你所期望的那样为你效劳。对于上面例子中的情况来说,最根本的问题在于:对矩形适用的规则(宽度的改变和高度没关系)不适用于正方形(宽度和高度必须相同)。但公有继承声称:对基类对象适用的任何东西 ---- 任何!---- 也适用于派生类对象。在矩形和正方形的例子(以及条款40中涉及到set的一个类似的例子)中,所声称的原则不适用,所以用公有继承来表示它们的关系只会是错误。当然,编译器不会阻拦你这样做,但正如我们所看到的,它不能保证程序可以工作正常。正如每个程序员都知道的,代码通过编译并不说明它能正常工作。</P>
<P>但也不要太担心你多年积累的软件开发直觉在步入到面向对象设计时会没有用武之地。那些知识还是很有价值,但既然你在自己的设计宝库中又增加了继承这一利器,你就要用新的眼光来扩展你的专业直觉,从而指导你开发出正确无误的面向对象程序。很快,你会觉得让Penguin从Bird继承或让Square从Rectangle 继承的想法很可笑,就象现在某个人向你展示一个长达数页的函数你会觉得可笑一样。也许它是解决问题的正确方法,只是不太合适。</P>
<P>当然,"是一个" 的关系不是存在于类之间的唯一关系。类之间常见的另两个关系是 "有一个" 和 "用...来实现"。这些关系在条款40和42进行讨论。这两个关系中的某一个被不正确地表示成 "是一个" 的情况并不少见,这将导致错误的设计。所以,一定要确保自己理解这些关系的区别,以及怎么最好地用C++来表示它们。<br>
</P>
</DIV></div></div>
</center></BODY></HTML>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -