📄 effective c++ 2e item43.htm
字号:
<TABLE cellSpacing=0 cellPadding=1 width=770 align=center bgColor=#eeeecc
border=1>
<TBODY>
<TR>
<TD align=left width=300><B>关键字:</B><BR>Effective C++ </TD>
<TD align=middle width=120><B>贴文时间</B><BR>2001-8-5 19:01:27 </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=9474">投票</A> </TD></TR>
<TR>
<TD> lostmouse 翻译 </TD>
<TD vAlign=top colSpan=3><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 class=fst align=left>
<DIV class=fstdiv3 id=print2><BR><BR>
<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> virtual int draw();</P>
<P> ...</P>
<P>};</P>
<P>class GraphicalObject {<BR>public:<BR> virtual int draw();</P>
<P> ...</P>
<P>};</P>
<P>class LotterySimulation: public
Lottery,<BR>
public GraphicalObject {</P>
<P>
...
// 没有声明draw</P>
<P>};</P>
<P>LotterySimulation *pls = new LotterySimulation;</P>
<P>pls->draw();
// 错误! ----
二义<BR>pls->Lottery::draw();
// 正确<BR>pls->GraphicalObject::draw(); // 正确</P>
<P>这段代码看起来很笨拙,但起码可以工作。遗憾的是,想避免这种笨拙很难。即使其中一个被继承的draw函数是私有成员从而不能被访问,二义还是存在。(对此有一个很好的理由来解释,但完整的说明在条款26中提供,所以此处不再重复。)</P>
<P>显式地限制修饰成员不仅很笨拙,而且还带来限制。当显式地用一个类名来限制修饰一个虚函数时,函数的行为将不再具有虚拟的特征。相反,被调用的函数只能是你所指定的那个,即使调用是作用在派生类的对象上:</P>
<P>class SpecialLotterySimulation: public LotterySimulation
{<BR>public:<BR> virtual int draw();</P>
<P> ...</P>
<P>};</P>
<P>pls = new SpecialLotterySimulation;</P>
<P>pls->draw();
// 错误! ----
还是有二义<BR>pls->Lottery::draw();
//
调用Lottery::draw<BR>pls->GraphicalObject::draw();
// 调用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> virtual int
lotteryDraw() = 0;</P>
<P> virtual int draw() { return lotteryDraw(); }<BR>};</P>
<P>class AuxGraphicalObject: public GraphicalObject {<BR>public:<BR>
virtual int graphicalObjectDraw() = 0;</P>
<P> virtual int draw() { return graphicalObjectDraw(); }<BR>};</P>
<P><BR>class LotterySimulation: public
AuxLottery,<BR>
public AuxGraphicalObject {<BR>public:<BR> virtual int
lotteryDraw();<BR> virtual int graphicalObjectDraw();</P>
<P> ...</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->draw();</P>
<P>// 调用LotterySimulation::graphicalObjectDraw<BR>pgo->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>
B
C<BR>
\
/<BR>
\
/<BR>
\/<BR>
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>
<P>
A
<BR>
/\<BR>
/
\<BR>
/
\<BR>
B
C<BR>
\
/<BR>
\
/<BR>
\/<BR>
D</P>
<P>钻石可能是女孩最好的朋友,也许不是;但肯定的是,象这样一种钻石形状的继承结构绝对不可能成为我们的朋友。如果创建了象这样的层次结构,就会立即面临这样一个问题:是不是该让A成为虚基类呢?即,从A的继承是否应该是虚拟的呢?现实中,答案几乎总是
---- 应该;只有极少数情况下会想让类型D的对象包含A的数据成员的多个拷贝。正是认识到这一事实,上面的B和C将A声明为虚基类。</P>
<P>遗憾的是,在定义B和C的时候,你可能不知道将来是否会有类去同时继承它们,而且知不知道这一点实际上对正确地定义这两个类没有必要。对类的设计者来说,这实在是进退两难。如果不将A声明为B和C的虚基类,今后D的设计者就有可能需要修改B和C的定义,以便更有效地使用它们。通常,这很难做到,因为A,B和C的定义往往是只读的。例如这样的情况:A,B和C在一个库中,而D由库的用户来写。</P>
<P>另一方面,如果真的将A声明为B和C的虚基类,往往会在空间和时间上强加给用户额外的开销。因为虚基类常常是通过对象指针来实现的,并非对象本身。自不必说,内存中对象的分布是和编译器相关的,但一条不变的事实是:如果A作为
"非虚" 基类,类型D的对象在内存中的分布通常占用连续的内存单元;如果A作为 "虚"
基类,有时,类型D的对象在内存中的分布占用连续的内存单元,但其中两个单元包含的是指针,指向包含虚基类数据成员的内存单元:</P>
<P>A是非虚基类时D对象通常的内存分布:</P>
<P> A部分+ B部分+ A部分 +
C部分 + D部分</P>
<P>A是虚基类时D对象在某些编译器下的内存分布:</P>
<P>
------------------------------------------------<BR>
|
|<BR>
|
+<BR> B部分 + 指针 + C部分 + 指针
+ D部分 +
A部分<BR>
|
+<BR>
|
|<BR>
------------------------</P>
<P>即使编译器不采用这种特殊的实现策略,使用虚继承通常也会带来某种空间上的惩罚。</P>
<P>考虑到这些因素,看来,在进行高效的类设计时如果涉及到MI,作为库的设计者就要具有超凡的远见。然而现在的年代,常识都日益成为了稀有品,因而你会不明智地过多依赖于语言特性,这就不仅要求设计者能够预计得到未来的需要,而且简直就是要你做到彻底的先知先觉(参见条款M32)。</P>
<P>当然,这也可以说成是在虚函数和非虚函数间选择,但还是有重大的不同。条款36说明,虚函数具有定义明确的高级含义,非虚函数也同样具有定义明确的高级含义,而且它们的含义有显著的不同,所以在清楚自己想对子类的设计者传达什么含义的基础上,在二者之间作出选择是可能的。但是,决定基类是否应该是虚拟的,则缺乏定义明确的高级含义;相反,决定通常取决于整个继承的层次结构,所以除非知道了整个层次结构,否则无法做出决定。如果正确地定义出个类之前需要清楚地知道将来怎么使用它,这种情况下将很难设计出高效的类。</P>
<P>就算避开了二义性问题,并且解决了是否应该从基类虚拟继承的疑问,还是会有许多复杂性问题等着你。为了长话短说,在此我仅提出应该记住的其它两点:</P>
<P>·
向虚基类传递构造函数参数。非虚继承时,基类构造函数的参数是由紧临的派生类的成员初始化列表指定的。因为单继承的层次结构只需要非虚基类,继承层次结构中参数的向上传递采用的是一种很自然的方式:第n层的类将参数传给第n-1层的类。但是,虚基类的构造函数则不同,它的参数是由继承结构中最底层派生类的成员初始化列表指定的。这就造成,负责初始化虚基类的那个类可能在继承图中和它相距很远;如果有新类增加到继承结构中,执行初始化的类还可能改变。(避免这个问题的一个好办法是:消除对虚基类传递构造函数参数的需要。最简单的做法是避免在这样的类中放入数据成员。这本质上是Java的解决之道:Java中的虚基类(即,"接口")禁止包含数据)</P>
<P>·
虚函数的优先度。就在你自认为弄清了所有的二义之时,它们却又在你面前摇身一变。再次看看关于类A,B,C和D的钻石形状的继承图。假设A定义了一个虚成员函数mf,C重定义了它;B和D则没有重定义mf:</P>
<P>
A virtual void
mf();<BR>
/\<BR> /
\<BR>
/ \<BR>
B C virtual void
mf();<BR>
\
/<BR> \
/<BR>
\/<BR>
D</P>
<P>根据以前的讨论,你会认为下面有二义:</P>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -