条款 39: 避免"向下转换" 继承层次
2015-01-02 20:46
218 查看
class Person { ... }; class BankAccount { public: BankAccount(const Person *primaryOwner, const Person *jointOwner); virtual ~BankAccount(); virtual void makeDeposit(double amount) = 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(); // 给帐户增加利息 ... };
银行想为它所有的帐户维持一个列表,这可能是通过标准库 (参见条款 49)中的 list 类模板实现的。假设列表被叫做 allAccounts:
list<BankAccount*> allAccounts; // 银行中所有帐户
// 不能通过编译的循环(如果你以前从没 // 见过使用"迭代子" 的代码,参见下文) for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { (*p)->creditInterest(); // 错误! }编译器很快就会让你认识到:allAccounts 包含的指针指向的是BankAccount 对象,而非 SavingsAccount 对象,所以每次循环,p 指向的是一个 BankAccount。这使得对 creditInterest 的调用无效,因为 creditInterest只是为
SavingsAccount 对象声明的,而不是 BankAccount。
的确,allAccounts 是被定义为保存BankAccount*,但要知道,上面的循环中它事实上保存的是 SavingsAccount*,因为
SavingsAccount 是仅有的可以被实例话的类。愚蠢的编译器!对我们来说这么显然的事情它竟然笨得一无所知。所以你决定告诉它:allAccounts 真的包含的是 SavingsAccount*:
// 可以通过编译的循环,但很糟糕 for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { <span style="color:#ff0000;">static_cast<SavingsAccount*>(*p)->creditInterest();</span> }
这种类型的转换---- 从一个基类指针到一个派生类指针---- 被称为"向下转换",因为它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧可以工作;但正如下面即将看到的,它将给今后的维护人员带来恶梦。
还是回到银行的话题上来。受到存款帐户业务大获成功的激励,银行决定再推出支票帐户业务。另外,假设支票帐户和存款帐户一样,也要负担利息:
class CheckingAccount: public BankAccount { public: void creditInterest(); // 给帐户增加利息 ... };不用说,allAccounts 现在是一个包含存款和支票两种帐户指针的列表。于是,上面所写的计算利息的循环转瞬间有了大麻烦。
第一个问题是,虽然新增了一个 CheckingAccount,但即使不去修改循环代码,编译还是可以继续通过。因为编译器只是简单地听信于你所告诉它们
(通过 static_cast)的一切: *p 指向的是 SavingsAccount*。谁叫你是它的主人呢?这会给今后维护带来第一个恶梦。维护期第二个恶梦在于,你一定想去解决这个问题,所以你会写出这样的代码:
for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { if (*p 指向一个SavingsAccount) static_cast<SavingsAccount*>(*p)->creditInterest(); else static_cast<CheckingAccount*>(*p)->creditInterest(); }任何时候发现自己写出"如果对象属于类型 T1,做某事;但如果属于类型T2,做另外某事" 之类的代码,就要扇自己一个耳光。这不是 C++的做法。是的,在 C,Pascal,甚至 Smalltalk 中,它是很合理的做法,但在 C++中不是。在 C++中,要使用虚函数。
总结:在C++中,函数操作想与类型绑定,只有通过虚函数机制才能实现。对于一个虚函数,编译器可以根据所使用对象的类型来保证正确的函数调用。
class BankAccount { ... }; // 同上
// 一个新类,表示要支付利息的帐户 class InterestBearingAccount: public BankAccount { public: virtual void creditInterest() = 0; ... };
class SavingsAccount: public InterestBearingAccount { ... // 同上 };
class CheckingAccount: public InterestBearingAccount { ... //as above };
用图形表示如下:
BankAccount
^
|
InterestBearingAccount
/\
/ \
/ \
CheckingAccount SavingsAccount
因为存款和支票账户都要支付利息,所以很自然地想到把这一共同行为转移到一个公共的基类中。但是,如果假设不是所有的银行帐户都需要支付利息(以我的经验,这当然是个合理的假设) ,就不能把它转移到 BankAccount 类中。所以,BankAccount 引入一个新的子类 InterestBearingAccount
,并使 SavingsAccoun 和 CheckingAccount 从它继承。
存款和支票账户都要支付利息的事实是通过 InterestBearingAccount 的纯虚函数 creditInterest 来体现的,它要在子类 SavingsAccount 和 CheckingAccount中重新定义。
有了新的类层次结构,就可以这样来重写循环代码:
// 好一些,但还不完美 for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { static_cast<InterestBearingAccount*>(*p)->creditInterest(); }尽管这个循环还是包含一个讨厌的转换,但代码已经比过去健壮多了,因为即使又增加 InterestBearingAccount 新的子类到程序中,它还是可以继续工作。
为了完全消除转换,就必须对设计做一些改变。一种方法是限制帐户列表的类型。如果能得到一列 InterestBearingAccount 对象而不是 BankAccount 对象,那就太好了:
// 银行中所有要支付利息的帐户 list<InterestBearingAccount*> allIBAccounts; // 可以通过编译且现在将来都可以工作的循环 for (list<InterestBearingAccount*>::iterator p = allIBAccounts.begin(); p != allIBAccounts.end(); ++p) { (*p)->creditInterest(); }
如果不想用上面这种"采用更特定的列表" 的方法,那就让 creditInterest操作使用于所有的银行帐户,但对于不用支付利息的帐户来说,它只是一个空操作。这个方法可以这样来表示:
class BankAccount { public: virtual void creditInterest() {} ... }; class SavingsAccount: public BankAccount { ... }; class CheckingAccount: public BankAccount { ... }; list<BankAccount*> allAccounts; // 看啊,没有转换! for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { (*p)->creditInterest(); }向下转换" 可以通过几种方法来消除:
最好的方法
是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,所以要使这些类的每个虚函数成为一个空操作。第二个方法是加强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。
尽管如此,还是有比上面那种原始转换更好的办法。这种方法称为"安全的向下转换",它通过 C++的 dynamic_cast 运算符(参见条款 M2)来实现。当对一个指针使用
dynamic_cast 时,先尝试转换,如果成功(即,指针的动态类型(见条款 38) 和正被转换的类型一致) ,就返回新类型的合法指针;如果dynamic_cast 失败,返回空指针。
class BankAccount { ... }; // 和本条款开始时一样 class SavingsAccount: // 同上 public BankAccount { ... }; class CheckingAccount: // 同上 public BankAccount { ... }; list<BankAccount*> allAccounts; // 看起来应该熟悉些了吧... void error(const string& msg); // 出错处理函数; // 见下文
// 嗯,至少转换很安全 for (list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) { // 尝试将*p 安全转换为 SavingsAccount*; // psa 的定义信息见下文 if (SavingsAccount *psa = dynamic_cast<SavingsAccount*>(*p)) { psa->creditInterest(); } // 尝试将它安全转换为 CheckingAccount else if (CheckingAccount *pca = dynamic_cast<CheckingAccount*>(*p)){ pca->creditInterest(); } // 未知的帐户类型 else { error("Unknown account type!"); } }
用 if-then-else 风格的编程来进行向下转换比用虚函数要逊色得多,应该将这种方法保留到万不得已的情况下使用。
总结:
继承层次中的向下转换有时是难以避免的,解决办法:
1、最优选择,用虚函数来编程
2、用C++的 dynamic_cast 运算符(参见条款 M2)来实现。
相关文章推荐
- effective c++ 条款39: 避免 "向下转换" 继承层次
- 条款39: 避免 "向下转换" 继承层次
- dynamic_cast 与 避免 "向下转换" 继承层次
- effective C++笔记之条款39: 避免“向下转换”继承层次
- 避免 "向下转换" 继承层次
- 条款 35: 使公有继承体现"是一个" 的含义
- 一个困惑的类层次设计根据:回调函数,避免向下类型转换(转)
- Effective C++ 第二版 37)不要重定义非虚函数 38)不要重定义缺省参数值 39)避免向下转换
- 佩特来项目经验小集合(2)___组合查询存储过程,报错 "varchar JBID='' 转换成数据类型 int 时失败"
- effective c++ 条款35: 使公有继承体现 "是一个" 的含义
- iOS 整型转换警告"NSInteger' should not be used as format arguments; add an explicit cast to 'long' inste"
- 公有继承意味着 "是一个",私有继承意味着"用...来实现(避免切片)"
- Creating dll for executing external procedure('c' language)", version Version 8i
- 还原数据库时发生错误,提示'逻辑文件 'XX' 不是数据库 'XX' 的一部分"之类的错误
- 也驳"驳'C语言已经死了'"
- Form表单中method="post/get'的区别
- 如何传递数组参数并避免数组"降价"
- 解决"无法找到脚本库 "/aspnet_client/system_web/1_1_4322/WebUIValidation.js'.请尝到试手动放置此文件,或通过运行"aspnet_regiis -c"重新安装。"?(转东转西)
- EXEC master..xp_cmdshell 'bcp inzool.dbo.Out_Dv_Topic out c:/temp1.xls -c -q -S"127.0.0.1" -U"sa" -P"111111"'
- 类型"string"的值无法转换为"System.Drawing.Color"