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

📄 9284.htm

📁 C++细节解释
💻 HTM
📖 第 1 页 / 共 2 页
字号:
<P>class ModelC: public Airplane {</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; // 没有声明fly函数<BR>};</P> 
<P>然后,在程序中,他们做了类似下面的事:</P> 
<P>Airport JFK(...);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // JFK是纽约市的一个机场</P> 
<P>Airplane *pa = new ModelC;</P> 
<P>...</P> 
<P>pa-&gt;fly(JFK);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 调用Airplane::fly!</P> 
<P>这将造成悲剧:竟然试图让ModelC对象如同ModelA或ModelB那样飞行。这种行为可不能换来旅客对你的信任!</P> 
<P>这里的问题不在于Airplane::fly具有缺省行为,而在于ModelC可以不用明确地声明就可以继承这一行为。幸运的是,可以很容易做到为子类提供缺省行为、同时只是在子类想要的时候才给它们。窍门在于切断虚函数的接口和它的缺省实现之间的联系。下面是一种方法:</P> 
<P>class Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination) = 0;</P> 
<P>&nbsp; ...</P> 
<P>protected:<BR>&nbsp; void defaultFly(const Airport&amp; destination);<BR>};</P> 
<P>void Airplane::defaultFly(const Airport&amp; destination)<BR>{<BR>&nbsp; 飞机飞往某一目的地的缺省代码<BR>}</P> 
<P>注意Airplane::fly已经变成了纯虚函数,它提供了飞行的接口。缺省实现还是存在于Airplane类中,但现在它是以一个独立函数(defaultFly)的形式存在的。ModelA和ModelB这些类想执行缺省行为的话,只用简单地在它们的fly函数体中对defaultFly进行一个内联调用(关于内联和虚函数间的相互关系,参见条款33):</P> 
<P>class ModelA: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination)<BR>&nbsp; { defaultFly(destination); }</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>class ModelB: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination)<BR>&nbsp; { defaultFly(destination); }</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>对于ModelC类来说,它不可能无意间继承不正确的fly实现。因为Airplane中的纯虚函数强迫ModelC提供它自己版本的fly。</P> 
<P>class ModelC: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination);<BR>&nbsp; ...</P> 
<P>};</P> 
<P>void ModelC::fly(const Airport&amp; destination)<BR>{<BR>&nbsp; ModelC飞往某一目的地的代码<BR>}</P> 
<P>这个方法不会万无一失(程序员还会因为 "拷贝粘贴" 而出错),但它比最初的设计可靠多了。至于Airplane::defaultFly被声明为protected,是因为它确实只是Airplane及其派生类的实现细节。使用airplane的用户只关心飞机能飞,而不会关心是怎么实现的。</P> 
<P>Airplane::defaultFly是一个非虚函数也很重要。因为没有子类会重新定义这个函数,条款37说明了这一事实。如果defaultFly为虚函数,就会又回到这个问题:如果某些子类应该重新定义defaultFly而又忘记去做,那该怎么办?</P> 
<P>一些人反对将接口和缺省实现作为单独函数分开,例如上面的fly和defaultFly。他们认为,起码这会污染类的名字空间,因为有这么多相近的函数名称在扩散。然而他们还是赞同接口和缺省实现应该分离。怎么解决这种表面上存在的矛盾呢?可以借助于这一事实:纯虚函数必须在子类中重新声明,但它还是可以在基类中有自己的实现。下面的Airplane正是利用这一点重新定义了一个纯虚函数:</P> 
<P>class Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination) = 0;</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>void Airplane::fly(const Airport&amp; destination)<BR>{<BR>&nbsp; 飞机飞往某一目的地的缺省代码<BR>}</P> 
<P>class ModelA: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination)<BR>&nbsp; { Airplane::fly(destination); }</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>class ModelB: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination)<BR>&nbsp; { Airplane::fly(destination); }</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>class ModelC: public Airplane {<BR>public:<BR>&nbsp; virtual void fly(const Airport&amp; destination);</P> 
<P>&nbsp; ...</P> 
<P>};</P> 
<P>void ModelC::fly(const Airport&amp; destination)<BR>{<BR>&nbsp; ModelC飞往某一目的地的代码<BR>}</P> 
<P>这一设计和前面的几乎一样,只是纯虚函数Airplane::fly的函数体取代了独立函数Airplane::defaultFly。从本质上说,fly已经被分成两个基本部分了。它的声明说明了它的接口(派生类必须使用),而它的定义说明了它的缺省行为(派生类可能会使用,但要明确地请求)。然而,将fly和defaultFly合并后,就不再能够为这两个函数声明不同的保护级别了:本来是protected的代码(在defaultFly中)现在成了public(因为它在fly中)。</P> 
<P>最后,来谈谈Shape的非虚函数,objectID。当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为 ---- 不管一个派生类有多特殊。所以,</P> 
<P>· 声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。</P> 
<P>可以认为,Shape::objectID的声明就是在说,"每个Shape对象有一个函数用来产生对象的标识符,并且对象标识符的产生方式总是一样的。这种方式由Shape::objectID的定义决定,派生类不能改变它。" 因为非虚函数表示一种特殊性上的不变性,所以它决不能在子类中重新定义,关于这一点条款37进行了讨论。</P> 
<P>理解了纯虚函数、简单虚函数和非虚函数在声明上的区别,就可以精确地指定你想让派生类继承什么:仅仅是接口,还是接口和一个缺省实现?或者,接口和一个强制实现?因为这些不同类型的声明指的是根本不同的事,所以在声明成员函数时一定要从中慎重选择。只有这样做,才可以避免没经验的程序员常犯的两个错误。</P> 
<P>第一个错误是把所有的函数都声明为非虚函数。这就使得派生类没有特殊化的余地;非虚析构函数尤其会出问题(参见条款14)。当然,设计出来的类不准备作为基类使用也是完全合理的(条款M34就给出了一个你会这样做的例子)。这种情况下,专门声明一组非虚成员函数是适当的。但是,把所有的函数都声明为非虚函数,大多数情况下是因为对虚函数和非虚函数之间区别的无知,或者是过分担心虚函数对程序性能的影响(参见条款M24)。而事实上是:几乎任何一个作为基类使用的类都有虚函数(再次参见条款14)。</P> 
<P>如果担心虚函数的开销,请允许我介绍80-20定律(参见条款M16)。它指出,在一个典型的程序中,80%的运行时间都花在执行20%的代码上。这条定律很重要,因为它意味着,平均起来,80%的函数调用可以是虚函数,并且它们不会对程序的整体性能带来哪怕一丁点可以觉察到的影响。所以,在担心是否承担得起虚函数的开销之前,不妨将注意力集中在那20%会真正带来影响的代码上。</P> 
<P>另一个常见的问题是将所有的函数都声明为虚函数。有时这没错 ---- 比如,协议类(Protocol class)就是证据(参见条款34)。但是,这样做往往表现了类的设计者缺乏表明坚定立场的勇气。一些函数不能在派生类中重定义,只要是这种情况,就要旗帜鲜明地将它声明为非虚函数。不能让你的函数好象可以为任何人做任何事 ---- 只要他们花点时间重新定义所有的函数。记住,如果有一个基类B,一个派生类D,和一个成员函数mf,那么下面每个对mf的调用都必须工作正常:</P> 
<P>D *pd = new D;<BR>B *pb = pd;</P> 
<P>pb-&gt;mf();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 通过基类指针调用mf</P> 
<P>pd-&gt;mf();&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; // 通过派生类指针调用mf</P> 
<P>有时,必须将mf声明为非虚函数才能保证一切都以你所期望的方式工作(参见条款37)。如果需要特殊性上的不变性,就大胆地说出来吧!<br> 
</P> 
</DIV></div></div> 
 
</center></BODY></HTML> 

⌨️ 快捷键说明

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