⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 9474.htm

📁 C++细节解释
💻 HTM
📖 第 1 页 / 共 3 页
字号:
<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 Item43
</title>
</HEAD>
<BODY >
<center>

<div align=center><div class=fst align=left><div class=fstdiv3 id=print2>
<b>
Effective C++ 2e Item43</b><P>条款43: 明智地使用多继承</P> 
<P>要看是谁来说,多继承(MI)要么被认为是神来之笔,要么被当成是魔鬼的造物。支持者宣扬说,它是对真实世界问题进行自然模型化所必需的;而批评者争论说,它太慢,难以实现,功能却不比单继承强大。更让人为难的是,面向对象编程语言领域在这个问题上至今仍存在分歧:C++,Eiffel和the Common LISP Object System (CLOS)提供了MI;Smalltalk,Objective C和Object Pascal没有提供;而Java只是提供有限的支持。可怜的程序员该相信谁呢?</P> 
<P>在相信任何事情之前,首先得弄清事实。C++中,关于MI一条不容争辩的事实是,MI的出现就象打开了潘朵拉的盒子,带来了单继承中绝对不会存在的复杂性。其中,最基本的一条是二义性(参见条款26)。如果一个派生类从多个基类继承了一个成员名,所有对这个名字的访问都是二义的;你必须明确地说出你所指的是哪个成员。下面的例子取自ARM(参见条款50)中的一个专题讨论:</P> 
<P>class Lottery {<BR>public:<BR>&nbsp; virtual int draw();</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>class GraphicalObject {<BR>public:<BR>&nbsp; virtual int draw();</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>class LotterySimulation: public Lottery,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public GraphicalObject {</P> 
<P>&nbsp; ...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 没有声明draw</P> 
<P>};</P> 
<P>LotterySimulation *pls = new LotterySimulation;</P> 
<P>pls-&gt;draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 错误! ---- 二义<BR>pls-&gt;Lottery::draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 正确<BR>pls-&gt;GraphicalObject::draw();&nbsp; // 正确</P> 
<P>这段代码看起来很笨拙,但起码可以工作。遗憾的是,想避免这种笨拙很难。即使其中一个被继承的draw函数是私有成员从而不能被访问,二义还是存在。(对此有一个很好的理由来解释,但完整的说明在条款26中提供,所以此处不再重复。)</P> 
<P>显式地限制修饰成员不仅很笨拙,而且还带来限制。当显式地用一个类名来限制修饰一个虚函数时,函数的行为将不再具有虚拟的特征。相反,被调用的函数只能是你所指定的那个,即使调用是作用在派生类的对象上:</P> 
<P>class SpecialLotterySimulation: public LotterySimulation {<BR>public:<BR>&nbsp; virtual int draw();</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>pls = new SpecialLotterySimulation;</P> 
<P>pls-&gt;draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 错误! ---- 还是有二义<BR>pls-&gt;Lottery::draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 调用Lottery::draw<BR>pls-&gt;GraphicalObject::draw();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 调用GraphicalObject::draw</P> 
<P>注意,在这种情况下,即使pls指向的是SpecialLotterySimulation对象,也无法(没有 "向下转换" ---- 参见条款39)调用这个类中定义的draw函数。</P> 
<P>没完,还有呢。Lottery和GraphicalObject中的draw函数都被声明为虚函数,所以子类可以重新定义它们(见条款36),但如果LotterySimulation想对二者都重新定义那该怎么办?令人沮丧的是,这不可能,因为一个类只允许有唯一一个没有参数、名称为draw的函数。(这个规则有个例外,即一个函数为const而另一个不是的时候 ---- 见条款21)</P> 
<P>从某一方面来说,这个问题很严重,严重到足以成为修改C++语言的理由。ARM中就讨论了一种可能,即,允许被继承的虚函数可以 "改名" ;但后来又发现,可以通过增加一对新类来巧妙地避开这个问题:</P> 
<P>class AuxLottery: public Lottery {<BR>public:<BR>&nbsp; virtual int lotteryDraw() = 0;</P> 
<P>&nbsp; virtual int draw() { return lotteryDraw(); }<BR>};</P> 
<P>class AuxGraphicalObject: public GraphicalObject {<BR>public:<BR>&nbsp; virtual int graphicalObjectDraw() = 0;</P> 
<P>&nbsp; virtual int draw() { return graphicalObjectDraw(); }<BR>};</P> 
<P><BR>class LotterySimulation: public AuxLottery,<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; public AuxGraphicalObject {<BR>public:<BR>&nbsp; virtual int lotteryDraw();<BR>&nbsp; virtual int graphicalObjectDraw();</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>这两个新类, AuxLottery和AuxGraphicalObject,本质上为各自继承的draw函数声明了新的名字。新名字以纯虚函数的形式提供,本例中即lotteryDraw和graphicalObjectDraw;函数是纯虚拟的,所以具体的子类必须重新定义它们。另外,每个类都重新定义了继承而来的draw函数,让它们调用新的纯虚函数。最终效果是,在这个类体系结构中,有二义的单个名字draw被有效地分成了无二义但功能等价的两个名字:lotteryDraw和graphicalObjectDraw:</P> 
<P>LotterySimulation *pls = new LotterySimulation;</P> 
<P>Lottery *pl = pls;<BR>GraphicalObject *pgo = pls;</P> 
<P>// 调用LotterySimulation::lotteryDraw<BR>pl-&gt;draw();</P> 
<P>// 调用LotterySimulation::graphicalObjectDraw<BR>pgo-&gt;draw();</P> 
<P>这是一个集纯虚函数,简单虚函数和内联函数(参见条款33)综合应用之大成的方法,值得牢记在心。首先,它解决了问题,这个问题说不定哪天你就会碰到。其次,它可以提醒你,使用多继承会导致复杂性。是的,这个方法解决了问题,但仅仅为了重新定义一个虚函数而不得不去引入新的类,你真的愿意这样做吗?AuxLottery和AuxGraphicalObject类对于保证类层次结构的正确运转是必需的,但它们既不对应于问题范畴(problem domain )的某个抽象,也不对应于实现范畴(implementation domain)的某个抽象。它们单纯是作为一种实现设备而存在,再没有别的用处。你一定知道,好的软件是 "设备无关" 的,这条法则在此也适用。</P> 
<P>将来使用MI还会面临更多的问题,二义性问题(尽管有趣)只不过是刚开始。另一个问题基于这样一个实践经验:一个起初象下面这样的继承层次结构:</P> 
<P>class B { ... };<BR>class C { ... };<BR>class D: public B, public C { ... };</P> 
<P>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; B&nbsp;&nbsp;&nbsp; C<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \&nbsp;&nbsp;&nbsp; /<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \&nbsp; /<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; \/<BR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; D</P> 
<P>往往最后悲惨地发展成象下面这样:</P> 
<P>class A { ... };<BR>class B : virtual public A { ... };<BR>class C : virtual public A { ... };<BR>class D: public B, public C { ... };</P> 

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -