📄 effective c++ 2e item39.htm
字号:
<TBODY>
<TR>
<TH align=left>Effective C++ 2e Item39
</TH></TD></TR></TBODY></TABLE>
<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-7-31 22:13:58 </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=9336">投票</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> </P>
<P>条款39: 避免 "向下转换" 继承层次</P>
<P>在当今喧嚣的经济时代,关注一下我们的金融机构是个不错的主意。所以,看看下面这个有关银行帐户的协议类(Protocol class
)(参见条款34):</P>
<P>class Person { ... };</P>
<P>class BankAccount {<BR>public:<BR> BankAccount(const Person
*primaryOwner,<BR>
const Person *jointOwner);<BR> virtual ~BankAccount();</P>
<P> virtual void makeDeposit(double amount) = 0;<BR> virtual void
makeWithdrawal(double amount) = 0;</P>
<P> virtual double balance() const = 0;</P>
<P> ...</P>
<P>};</P>
<P>很多银行现在提供了多种令人眼花缭乱的帐户类型,但为简化起见,我们假设只有一种银行帐户,称为存款帐户:</P>
<P>class SavingsAccount: public BankAccount {<BR>public:<BR>
SavingsAccount(const Person
*primaryOwner,<BR>
const Person *jointOwner);<BR> ~SavingsAccount();</P>
<P> void
creditInterest();
// 给帐户增加利息</P>
<P> ...</P>
<P>};</P>
<P>这远远称不上是一个真正的存款帐户,但还是那句话,现在什么年代?至少,它满足我们现在的需要。</P>
<P>银行想为它所有的帐户维持一个列表,这可能是通过标准库(参见条款49)中的list类模板实现的。假设列表被叫做allAccounts:</P>
<P>list<BankAccount*>
allAccounts; // 银行中所有帐户</P>
<P>和所有的标准容器一样,list存储的是对象的拷贝,所以,为避免每个BankAccount存储多个拷贝,银行决定让allAccounts保存BankAccount的指针,而不是BankAccount本身。</P>
<P>假设现在准备写一段代码来遍历所有的帐户,为每个帐户计算利息。你会这么写:</P>
<P>// 不能通过编译的循环(如果你以前从没<BR>// 见过使用 "迭代子" 的代码,参见下文)<BR>for
(list<BankAccount*>::iterator p =
allAccounts.begin();<BR> p !=
allAccounts.end();<BR> ++p) {</P>
<P> (*p)->creditInterest(); // 错误!</P>
<P>}</P>
<P>但是,编译器很快就会让你认识到:allAccounts包含的指针指向的是BankAccount对象,而非SavingsAccount对象,所以每次循环,p指向的是一个BankAccount。这使得对creditInterest的调用无效,因为creditInterest只是为SavingsAccount对象声明的,而不是BankAccount。</P>
<P>如果"list<BankAccount*>::iterator p = allAccounts.begin()"
在你看来更象电话线中的噪音,而不是C++,那很显然,你以前无缘见识过C++标准库中的容器类模板。标准库中的这一部分通常被称为标准模板库(STL),你可以在条款49和M35初窥其概貌。但现在你只用知道,变量p工作起来就象一个指针,它将allAccounts中的元素从头到尾循环一遍。也就是说,p工作起来就好象它的类型是BankAccount**而列表中的元素都存储在一个数组中。</P>
<P>上面的循环不能通过编译很令人泄气。的确,allAccounts是被定义为保存BankAccount*s,但要知道,上面的循环中它事实上保存的是SavingsAccount*s,因为SavingsAccount是仅有的可以被实例话的类。愚蠢的编译器!对我们来说这么显然的事情它竟然笨得一无所知。所以你决定告诉它:allAccounts真的包含的是SavingsAccount*s:</P>
<P>// 可以通过编译的循环,但很糟糕<BR>for (list<BankAccount*>::iterator p =
allAccounts.begin();<BR> p !=
allAccounts.end();<BR> ++p) {</P>
<P> static_cast<SavingsAccount*>(*p)->creditInterest();</P>
<P>}</P>
<P>一切问题迎刃而解!解决得很清晰,很漂亮,很简明,所做的仅仅是一个简单的转换而已。你知道allAccounts指针保存的是什么类型的指针,迟钝的编译器不知道,所以你通过一个转换来告诉它,还有比这更合理的事吗?</P>
<P>在此,我要拿圣经的故事做比喻。转换之于C++程序员,就象苹果之于夏娃。</P>
<P>这种类型的转换 ---- 从一个基类指针到一个派生类指针 ---- 被称为
"向下转换",因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正如下面即将看到的,它将给今后的维护人员带来恶梦。</P>
<P>还是回到银行的话题上来。受到存款帐户业务大获成功的激励,银行决定再推出支票帐户业务。另外,假设支票帐户和存款帐户一样,也要负担利息:</P>
<P>class CheckingAccount: public BankAccount {<BR>public:<BR> void
creditInterest(); // 给帐户增加利息</P>
<P> ...</P>
<P>};</P>
<P>不用说,allAccounts现在是一个包含存款和支票两种帐户指针的列表。于是,上面所写的计算利息的循环转瞬间有了大麻烦。</P>
<P>第一个问题是,虽然新增了一个CheckingAccount,但即使不去修改循环代码,编译还是可以继续通过。因为编译器只是简单地听信于你所告诉它们(通过static_cast)的一切:*p指向的是SavingsAccount*。谁叫你是它的主人呢?这会给今后维护带来第一个恶梦。维护期第二个恶梦在于,你一定想去解决这个问题,所以你会写出这样的代码:</P>
<P>for (list<BankAccount*>::iterator p =
allAccounts.begin();<BR> p !=
allAccounts.end();<BR> ++p) {</P>
<P> if (*p 指向一个 SavingsAccount)<BR>
static_cast<SavingsAccount*>(*p)->creditInterest();<BR>
else<BR>
static_cast<CheckingAccount*>(*p)->creditInterest();</P>
<P>}</P>
<P>任何时候发现自己写出 "如果对象属于类型T1,做某事;但如果属于类型T2,做另外某事"
之类的代码,就要扇自己一个耳光。这不是C++的做法。是的,在C,Pascal,甚至Smalltalk中,它是很合理的做法,但在C++中不是。在C++中,要使用虚函数。</P>
<P>记得吗?对于一个虚函数,编译器可以根据所使用对象的类型来保证正确的函数调用。所以不要在代码中随处乱扔条件语句或开关语句;让编译器来为你效劳。如下所示:</P>
<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>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -