条款39:避免“向下转换”继承层次

上传人:kms****20 文档编号:37575136 上传时间:2018-04-18 格式:DOC 页数:12 大小:39.50KB
返回 下载 相关 举报
条款39:避免“向下转换”继承层次_第1页
第1页 / 共12页
条款39:避免“向下转换”继承层次_第2页
第2页 / 共12页
条款39:避免“向下转换”继承层次_第3页
第3页 / 共12页
条款39:避免“向下转换”继承层次_第4页
第4页 / 共12页
条款39:避免“向下转换”继承层次_第5页
第5页 / 共12页
点击查看更多>>
资源描述

《条款39:避免“向下转换”继承层次》由会员分享,可在线阅读,更多相关《条款39:避免“向下转换”继承层次(12页珍藏版)》请在金锄头文库上搜索。

1、条款条款 3939:避免:避免“向下转换向下转换”继承层次继承层次条款 39: 避免 “向下转换“ 继承层次在当今喧嚣的经济时代,关注一下我们的金融机构是个不错的主意。所以,看看下面这个有关银行帐户的协议类(Protocol class )(参见条款 34):class Person . ;class BankAccount public:BankAccount(const Person *primaryOwner,const Person *jointOwner);virtual BankAccount();virtual void makeDeposit(double amount) =

2、0;virtual void makeWithdrawal(double amount) = 0;virtual double balance() const = 0;.;很多银行现在提供了多种令人眼花缭乱的帐户类型,但为简化起见,我们假设只有一种银行帐户,称为存款帐户:class SavingsAccount: public BankAccount public:SavingsAccount(const Person *primaryOwner,const Person *jointOwner);SavingsAccount();void creditInterest(); / 给帐户增加利

3、息.;这远远称不上是一个真正的存款帐户,但还是那句话,现在什么年代?至少,它满足我们现在的需要。银行想为它所有的帐户维持一个列表,这可能是通过标准库(参见条款 49)中的 list 类模板实现的。假设列表被叫做 allAccounts:list allAccounts; / 银行中所有帐户和所有的标准容器一样,list 存储的是对象的拷贝,所以,为避免每个 BankAccount 存储多个拷贝,银行决定让 allAccounts 保存BankAccount 的指针,而不是 BankAccount 本身。假设现在准备写一段代码来遍历所有的帐户,为每个帐户计算利息。你会这么写:/ 不能通过编译的循

4、环(如果你以前从没/ 见过使用 “迭代子“ 的代码,参见下文)for (list:iterator p = allAccounts.begin();p != allAccounts.end();+p) (*p)-creditInterest(); / 错误!但是,编译器很快就会让你认识到:allAccounts 包含的指针指向的是 BankAccount 对象,而非 SavingsAccount 对象,所以每次循环,p 指向的是一个 BankAccount。这使得对 creditInterest 的调用无效,因为 creditInterest 只是为 SavingsAccount 对象声明的,

5、而不是 BankAccount。如果“list:iterator p = allAccounts.begin()“ 在你看来更象电话线中的噪音,而不是 C+,那很显然,你以前无缘见识过 C+标准库中的容器类模板。标准库中的这一部分通常被称为标准模板库(STL) ,你可以在条款 49 和 M35 初窥其概貌。但现在你只用知道,变量 p 工作起来就象一个指针,它将 allAccounts中的元素从头到尾循环一遍。也就是说,p 工作起来就好象它的类型是 BankAccount*而列表中的元素都存储在一个数组中。上面的循环不能通过编译很令人泄气。的确,allAccounts 是被定义为保存 BankA

6、ccount*,但要知道,上面的循环中它事实上保存的是 SavingsAccount*,因为 SavingsAccount 是仅有的可以被实例话的类。愚蠢的编译器!对我们来说这么显然的事情它竟然笨得一无所知。所以你决定告诉它:allAccounts 真的包含的是SavingsAccount*:/ 可以通过编译的循环,但很糟糕for (list:iterator p = allAccounts.begin();p != allAccounts.end();+p) static_cast(*p)-creditInterest();一切问题迎刃而解!解决得很清晰,很漂亮,很简明,所做的仅仅是一个简单

7、的转换而已。你知道 allAccounts 指针保存的是什么类型的指针,迟钝的编译器不知道,所以你通过一个转换来告诉它,还有比这更合理的事吗?在此,我要拿圣经的故事做比喻。转换之于 C+程序员,就象苹果之于夏娃。这种类型的转换 - 从一个基类指针到一个派生类指针 - 被称为 “向下转换“,因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正如下面即将看到的,它将给今后的维护人员带来恶梦。还是回到银行的话题上来。受到存款帐户业务大获成功的激励,银行决定再推出支票帐户业务。另外,假设支票帐户和存款帐户一样,也要负担利息:class CheckingAccount: publ

8、ic BankAccount public:void creditInterest(); / 给帐户增加利息.;不用说,allAccounts 现在是一个包含存款和支票两种帐户指针的列表。于是,上面所写的计算利息的循环转瞬间有了大麻烦。第一个问题是,虽然新增了一个 CheckingAccount,但即使不去修改循环代码,编译还是可以继续通过。因为编译器只是简单地听信于你所告诉它们(通过 static_cast)的一切:*p 指向的是SavingsAccount*。谁叫你是它的主人呢?这会给今后维护带来第一个恶梦。维护期第二个恶梦在于,你一定想去解决这个问题,所以你会写出这样的代码:for (l

9、ist:iterator p = allAccounts.begin();p != allAccounts.end();+p) if (*p 指向一个 SavingsAccount)static_cast(*p)-creditInterest();elsestatic_cast(*p)-creditInterest();任何时候发现自己写出 “如果对象属于类型 T1,做某事;但如果属于类型 T2,做另外某事“ 之类的代码,就要扇自己一个耳光。这不是 C+的做法。是的,在 C,Pascal,甚至 Smalltalk 中,它是很合理的做法,但在 C+中不是。在 C+中,要使用虚函数。记得吗?对于一

10、个虚函数,编译器可以根据所使用对象的类型来保证正确的函数调用。所以不要在代码中随处乱扔条件语句或开关语句;让编译器来为你效劳。如下所示:class BankAccount . ; / 同上/ 一个新类,表示要支付利息的帐户class InterestBearingAccount: public BankAccount public:virtual void creditInterest() = 0;.;class SavingsAccount: public InterestBearingAccount . / 同上;class CheckingAccount: public Interest

11、BearingAccount . / as above;用图形表示如下:BankAccount|InterestBearingAccount / / CheckingAccount SavingsAccount因为存款和支票账户都要支付利息,所以很自然地想到把这一共同行为转移到一个公共的基类中。但是,如果假设不是所有的银行帐户都需要支付利息(以我的经验,这当然是个合理的假设) ,就不能把它转移到 BankAccount 类中。所以,要为 BankAccount 引入一个新的子类 InterestBearingAccount,并使 SavingsAccoun 和CheckingAccount 从

12、它继承。存款和支票账户都要支付利息的事实是通过InterestBearingAccount 的纯虚函数 creditInterest 来体现的,它要在子类 SavingsAccount 和 CheckingAccount 中重新定义。有了新的类层次结构,就可以这样来重写循环代码:/ 好一些,但还不完美for (list:iterator p = allAccounts.begin();p != allAccounts.end();+p) static_cast(*p)-creditInterest();尽管这个循环还是包含一个讨厌的转换,但代码已经比过去健壮多了,因为即使又增加 Interes

13、tBearingAccount 新的子类到程序中,它还是可以继续工作。为了完全消除转换,就必须对设计做一些改变。一种方法是限制帐户列表的类型。如果能得到一列 InterestBearingAccount 对象而不是 BankAccount 对象,那就太好了:/ 银行中所有要支付利息的帐户list allIBAccounts;/ 可以通过编译且现在将来都可以工作的循环for (list:iterator p =allIBAccounts.begin();p != allIBAccounts.end();+p) (*p)-creditInterest();如果不想用上面这种 “采用更特定的列表“

14、的方法,那就让creditInterest 操作使用于所有的银行帐户,但对于不用支付利息的帐户来说,它只是一个空操作。这个方法可以这样来表示:class BankAccount public:virtual void creditInterest() .;class SavingsAccount: public BankAccount . ;class CheckingAccount: public BankAccount . ;list allAccounts;/ 看啊,没有转换!for (list:iterator p = allAccounts.begin();p != allAccou

15、nts.end();+p) (*p)-creditInterest();要注意的是,虚函数 BankAccount:creditInterest 提供一个了空的缺省实现。这可以很方便地表示,它的行为在缺省情况下是一个空操作;但这也会给它本身带来难以预见的问题。想知道内幕,以及如何消除这一危险,请参考条款 36。还要注意的是,creditInterest 是一个(隐式的)内联函数,这本身没什么问题;但因为它同时又是一个虚函数,内联指令就有可能被忽略。条款 33解释了为什么。正如上面已经看到的,“向下转换“ 可以通过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,无论费多大工夫都是值得的,因为向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护(参见条款 M32) 。至此,我所说的都是事实;但,不是全部事实。有些情况下,真的不得不执行向下转换。例如,假设还是面临本条款开始的那种情况,即,allAccounts 保存 BankAccount 指针,creditInterest 只是为 SavingsAccount 对象定义,要写一个循环来为每个帐户计算利息。进一步假设,你不能

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 生活休闲 > 科普知识

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号