📄 9336.htm
字号:
<P>class BankAccount { ... }; // 同上</P>
<P>// 一个新类,表示要支付利息的帐户<BR>class InterestBearingAccount: public BankAccount {<BR>public:<BR> virtual void creditInterest() = 0;</P>
<P> ...</P>
<P>};</P>
<P>class SavingsAccount: public InterestBearingAccount {</P>
<P> ... // 同上</P>
<P>};</P>
<P>class CheckingAccount: public InterestBearingAccount {</P>
<P> ... // as above</P>
<P>};</P>
<P>用图形表示如下:</P>
<P> BankAccount<BR> ^<BR> |<BR> InterestBearingAccount <BR> /\<BR> / \<BR> / \<BR> CheckingAccount SavingsAccount</P>
<P>因为存款和支票账户都要支付利息,所以很自然地想到把这一共同行为转移到一个公共的基类中。但是,如果假设不是所有的银行帐户都需要支付利息(以我的经验,这当然是个合理的假设),就不能把它转移到BankAccount类中。所以,要为BankAccount引入一个新的子类InterestBearingAccount,并使SavingsAccoun和CheckingAccount从它继承。</P>
<P>存款和支票账户都要支付利息的事实是通过InterestBearingAccount的纯虚函数creditInterest来体现的,它要在子类SavingsAccount和CheckingAccount中重新定义。</P>
<P>有了新的类层次结构,就可以这样来重写循环代码:</P>
<P>// 好一些,但还不完美<BR>for (list<BankAccount*>::iterator p = allAccounts.begin();<BR> p != allAccounts.end();<BR> ++p) {</P>
<P> static_cast<InterestBearingAccount*>(*p)->creditInterest();</P>
<P>}</P>
<P>尽管这个循环还是包含一个讨厌的转换,但代码已经比过去健壮多了,因为即使又增加InterestBearingAccount新的子类到程序中,它还是可以继续工作。</P>
<P>为了完全消除转换,就必须对设计做一些改变。一种方法是限制帐户列表的类型。如果能得到一列InterestBearingAccount对象而不是BankAccount对象,那就太好了:</P>
<P>// 银行中所有要支付利息的帐户<BR>list<InterestBearingAccount*> allIBAccounts;</P>
<P>// 可以通过编译且现在将来都可以工作的循环<BR>for (list<InterestBearingAccount*>::iterator p =<BR> allIBAccounts.begin();<BR> p != allIBAccounts.end();<BR> ++p) {</P>
<P> (*p)->creditInterest();</P>
<P>}</P>
<P>如果不想用上面这种 "采用更特定的列表" 的方法,那就让creditInterest操作使用于所有的银行帐户,但对于不用支付利息的帐户来说,它只是一个空操作。这个方法可以这样来表示:</P>
<P>class BankAccount {<BR>public:<BR> virtual void creditInterest() {}</P>
<P> ...</P>
<P>};</P>
<P>class SavingsAccount: public BankAccount { ... };<BR>class CheckingAccount: public BankAccount { ... };<BR>list<BankAccount*> allAccounts;<BR>// 看啊,没有转换!<BR>for (list<BankAccount*>::iterator p = allAccounts.begin();<BR> p != allAccounts.end();<BR> ++p) {</P>
<P> (*p)->creditInterest();</P>
<P>}</P>
<P>要注意的是,虚函数BankAccount::creditInterest提供一个了空的缺省实现。这可以很方便地表示,它的行为在缺省情况下是一个空操作;但这也会给它本身带来难以预见的问题。想知道内幕,以及如何消除这一危险,请参考条款36。还要注意的是,creditInterest是一个(隐式的)内联函数,这本身没什么问题;但因为它同时又是一个虚函数,内联指令就有可能被忽略。条款33解释了为什么。</P>
<P>正如上面已经看到的,"向下转换" 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的,因为向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护(参见条款M32)。</P>
<P>至此,我所说的都是事实;但,不是全部事实。有些情况下,真的不得不执行向下转换。</P>
<P>例如,假设还是面临本条款开始的那种情况,即,allAccounts保存BankAccount指针,creditInterest只是为SavingsAccount对象定义,要写一个循环来为每个帐户计算利息。进一步假设,你不能改动这些类;你不能改变BankAccount,SavingsAccount或allAccounts的定义。(如果它们在某个只读的库中定义,就会出现这种情况)如果是这样的话,你就只有使用向下转换了,无论你认为这个办法有多丑陋。</P>
<P>尽管如此,还是有比上面那种原始转换更好的办法。这种方法称为 "安全的向下转换",它通过C++的dynamic_cast运算符(参见条款M2)来实现。当对一个指针使用dynamic_cast时,先尝试转换,如果成功(即,指针的动态类型(见条款38)和正被转换的类型一致),就返回新类型的合法指针;如果dynamic_cast失败,返回空指针。</P>
<P>下面就是加上了 "安全向下转换" 的例子:</P>
<P>class BankAccount { ... }; // 和本条款开始时一样</P>
<P>class SavingsAccount: // 同上<BR> public BankAccount { ... };</P>
<P>class CheckingAccount: // 同上<BR> public BankAccount { ... };</P>
<P>list<BankAccount*> allAccounts; // 看起来应该熟悉些了吧...</P>
<P>void error(const string& msg); // 出错处理函数;<BR> // 见下文</P>
<P>// 嗯,至少转换很安全<BR>for (list<BankAccount*>::iterator p = allAccounts.begin();<BR> p != allAccounts.end();<BR> ++p) {</P>
<P> // 尝试将*p安全转换为SavingsAccount*;<BR> // psa的定义信息见下文<BR> if (SavingsAccount *psa =<BR> dynamic_cast<SavingsAccount*>(*p)) {<BR> psa->creditInterest();<BR> }</P>
<P> // 尝试将它安全转换为CheckingAccount<BR> else if (CheckingAccount *pca =<BR> dynamic_cast<CheckingAccount*>(*p)) {<BR> pca->creditInterest();<BR> }</P>
<P> // 未知的帐户类型<BR> else {<BR> error("Unknown account type!");<BR> }<BR>}</P>
<P>这种方法远不够理想,但至少可以检测到转换失败,而用dynamic_cast是无法做到的。但要注意,对所有转换都失败的情况也要检查。这正是上面代码中最后一个else语句的用意所在。采用虚函数,就不必进行这样的检查,因为每个虚函数调用必然都会被解析为某个函数。然而,一旦打算进行转换,这一切好处都化为乌有。例如,如果某个人在类层次结构中增加了一种新类型的帐户,但又忘了更新上面的代码,所有对它的转换就会失败。所以,处理这种可能发生的情况十分重要。大部分情况下,并非所有的转换都会失败;但是,一旦允许转换,再好的程序员也会碰上麻烦。</P>
<P>上面if语句的条件部分,有些看上去象变量定义的东西,看到它你是不是慌张地擦了擦眼镜?如果真这样,别担心,你没看错。这种定义变量的方法是和dynamic_cast同时增加到C++语言中的。这一特性使得写出的代码更简洁,因为对psa或pca来说,它们只有在被dynamic_cast成功初始化的情况下,才会真正被用到;使用新的语法,就不必在(包含转换的)条件语句外定义这些变量。(条款32解释了为什么通常要避免多余的变量定义)如果编译器尚不支持这种定义变量的新方法,可以按老方法来做:</P>
<P>for (list<BankAccount*>::iterator p = allAccounts.begin();<BR> p != allAccounts.end();<BR> ++p) {</P>
<P> SavingsAccount *psa; // 传统定义<BR> CheckingAccount *pca; // 传统定义</P>
<P> if (psa = dynamic_cast<SavingsAccount*>(*p)) {<BR> psa->creditInterest();<BR> }</P>
<P> else if (pca = dynamic_cast<CheckingAccount*>(*p)) {<BR> pca->creditInterest();<BR> }</P>
<P> else {<BR> error("Unknown account type!");<BR> }<BR>}</P>
<P>当然,从处理事情的重要性来说,把psa和pca这样的变量放在哪儿定义并不十分重要。重要之处在于:用if-then-else风格的编程来进行向下转换比用虚函数要逊色得多,应该将这种方法保留到万不得已的情况下使用。运气好的话,你的程序世界里将永远看不到这样悲惨荒凉的景象。<br>
</P>
</DIV></div></div>
</center></BODY></HTML>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -