Effective C++ 第二版 37)不要重定义非虚函数 38)不要重定义缺省参数值 39)避免向下转换
来源:互联网 发布:淘宝商城工艺品 编辑:程序博客网 时间:2024/06/04 19:57
条款37 决不要重新定义继承而来的非虚函数
实践依据
假设类D公有继承于类B, B中定义了公有成员函数mf; mf的参数和返回类型假设为void;
class
B {
public
:
void
mf();
...
};
class
D:
public
B { ... };
//
D x;
// x 是类型D 的一个对象
那么, 如果这么做: ]
B *pB = &x;
// 得到x 的指针
pB->mf();
// 通过指针调用mf
和下面这么做的执行行为不同, 这一定会出乎意料;
D *pD = &x;
// 得到x 的指针
pD->mf();
// 通过指针调用 mf
因为两种情况下调用的都是对象x的成员函数mf; 但是如果mf是非虚函数而D又定义了自己的mf, 行为就会不同;
class
D:
public
B {
public
:
void
mf();
// 隐藏了B::mf; 参见条款50
...
};
pB->mf();
// 调用B::mf
pD->mf();
// 调用 D::mf
原因:
B::mf和D::mf作为非虚函数是静态绑定的(条款38); 因为pB被声明为指向B的指针类型, 通过pB调用非虚函数将调用B中的函数, 即使pB指向的是派生类对象;
相反, 虚函数是动态绑定的, 所以不会有这种问题; 如果mf是虚函数, 通过pB或pD调用mf都将调用D::mf, 因为pB和pD实际上指向的都是类型D的对象;
结论:
如果写D时重新定义了从B继承的非虚函数mf, D的对象就可能表现出分裂的行为; D的对象在mf被调用时, 行为有可能像B, 也有可能像D, 决定因素和对象本身没有关系, 而是取决于指向它的指针所声明的类型; 引用和指针情况一样;
理论依据
条款35说明公有继承的含义是Is-a, 条款36说明类中声明非虚函数实际上是建立了特殊的不变性; 那么: 适用于B对象的一切也适用于D对象, 因为D对象Is-a B对象; B的子类必须同时继承mf的接口和实现, 因为mf是非虚;
如果D重新定义了mf, 设计中就产生矛盾: 如果D是需要实现和B不同的mf, 而且每个B的对象是需要使用B实现的mf, 那么D对象将不是Is-a B对象; 这种情况D不能从B公有继承; 反之, 如果D必须从B公有继承, 而且D需要和B不同的mf的实现, 那么mf应该作为虚函数; 最后: 如果每个D Is-a B, 并且mf真的为B建立了特殊的不变性, 那么D就不能重新定义mf;
Note 任何条件下都要禁止重新定义继承而来的非虚函数;
条款38 决不要重新定义继承而来的缺省参数值
缺省参数只能作为函数的一部分而存在; 只有2种函数可以继承: 虚函数和非虚函数; 重定义缺省参数值的唯一方法是重定义一个继承而来的函数, 重定义非虚函数是一种错误(条款37); 所以, 范围缩小为"继承一个有确实参数值的虚函数";
理由: 虚函数是动态绑定的, 而缺少参数值是静态绑定的;
对象的静态类型指声明的存在于程序代码文本中的类型;
enum
ShapeColor { RED, GREEN, BLUE };
// 一个表示几何形状的类
class
Shape {
public
:
// 所有的形状都要提供一个函数绘制它们本身
virtual
void
draw(ShapeColor color = RED)
const
= 0;
...
};
class
Rectangle:
public
Shape {
public
:
// 注意:定义了不同的缺省参数值 ---- 不好!
virtual
void
draw(ShapeColor color = GREEN)
const
;
...
};
class
Circle:
public
Shape {
public
:
virtual
void
draw(ShapeColor color)
const
;
...
};
使用指针:
Shape *ps;
// 静态类型 = Shape*
Shape *pc =
new
Circle;
// 静态类型 = Shape*
Shape *pr =
new
Rectangle;
// 静态类型 = Shape*
>ps, pc, pr都声明为Shape*类型, 他们都以此作为自己的静态类型;
Note 这和他们真正所指向的对象的类型没有关系, 他们的静态类型总是Shape*;
对象的动态类型是由他当前所指的对象的类型决定的; 对象的动态类型表示他将执行何种行为;
>pc的动态类型是Circle*, pr是Rectangle*, ps没有动态类型, 因为他没有指向任何对象;
Note 动态类型可以在程序运行时改变, 典型的方式是赋值:
ps = pc;
// ps 的动态类型// 现在是 Circle*
ps = pr;
// ps 的动态类型// 现在是 Rectangle*
虚函数是动态绑定的, 虚函数通过哪个对象被调用, 具体被调用的函数由那个对象的动态类型决定:
pc->draw(RED);
// 调用Circle::draw(RED)
pr->draw(RED);
// 调用Rectangle::draw(RED)
将虚函数和确实参数值结合起来分析, 可能出现的情况:调用的是一个定义在派生类, 但使用了基类中的缺省参数值的虚函数;
pr->draw();
// 调用 Rectangle::draw(RED)!
>pr的动态类型是Rectangle*, 在Rectangle::draw中缺省参数值是GREEN; 但是由于pr的静态类型是Shape*, 函数调用的参数值是从Shape类中取得的, 而不从Rectangle; 结果非常不符合直觉!
无论ps, pc, pr是指针还是引用都会出现问题, 问题酒出在: draw是一个虚函数, 并且他的缺省参数在子类被重新定义了;
C++坚持这种有违常规的做法是出于运行效率的考虑; 如果缺省参数值被动态绑定, 编译器就必须想办法为虚函数在运行时确定合适的缺省值, 这将比现在采用的在编译阶段确定缺省值的机制更慢更复杂; 这个选择是为了速度的提高和实现的简便;
[Java完全就不支持缺省参数, 所以没这类问题 - -!, 但是Java可以用重载的方式实现缺省值, 虽然麻烦, 反而更安全些]
条款39 避免"向下转换"继承层次
e.g. 有关银行账户的协议类 Protocol class (条款34)
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();
// 给帐户增加利息
...
};
银行向为所有的账户维持一个列表, 可能通过标准库list类模板实现, 假设列表叫做allAccounts:
list<BankAccount*> allAccounts;
// 银行中所有帐户
和所有标准容器一样, list存储的是对象的拷贝; 为避免每个BankAccount存储多个拷贝, 让allAccounts保存BankAccount的指针, 而不是自身;
遍历所有账户, 为每个账户计算利息:
// 不能通过编译的循环(如果你以前从没 // 见过使用 "迭代子" 的代码,参见下文)
for
(list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p)
{
(*p)->creditInterest();
// 错误!
}
>allAccounts包含的指针指向BankAccount对象, 而不是SavingsAccount对象, p指向一个BankAccount, 对creditInterest调用无效;
list<BankAccount*>::iterator p = allAccounts.begin()是标准模板库STL的使用; p工作起来就像一个指针, 将allAccounts中的元素从头到尾循环一遍; p工作起来就好像他的类型是BankAccount**, 列表中的元素都好像存储在一个数组中;
>上例的循环中, allAccounts定义为保存BankAccount*, 但实际上保存的是SavingsAccount*, 因为BankAccount是抽象类, 只有SavingsAccount可以实例化;
显示地告诉编译器, 列表存储的是SavingsAccount*:
// 可以通过编译的循环,但很糟糕
for
(list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p)
{
static_cast
<SavingsAccount*>(*p)->creditInterest();
}
这种类型的转换--从一个基类指针到一个派生类指针--被称为"向下转换", 因为它向下转换了继承的层次结构; 上例中, 向下转换碰巧可以工作, 但它将给之后的维护带来恶果;
支票账户业务, 假设支票账户和存款账户一样, 也负担利息:
class
CheckingAccount:
public
BankAccount {
public
:
void
creditInterest();
// 给帐户增加利息
...
};
>allAccounts现在是一个包含存款和支票两种账户指针的列表;
于是上面的利息计算循环就出了问题:
虽然增加了CheckingAccount, 不修改循环代码, 编译还是可以通过; 编译器只是简单地语法和有效性检查, 接受 *p指向的是SavingAccount*的指示;
于是代码维护时, 有可能出现这样的代码:
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, doSomeThingA; 但如果属于类型T2, doSomeThingB"这样的代码, 就不属于C++的风格; 在C, Pascal, Smalltalk中这样做合理, 但在C++中有虚函数;
对于虚函数, 编译器可以根据对象的类型来保证正确的函数调用, 所以不用写条件或开关语句, 让编译器来决定:
class
BankAccount { ... };
// 同上
// 一个新类,表示要支付利息的帐户
class
InterestBearingAccount:
public
BankAccount {
public
:
virtual
void
creditInterest() = 0;
...
};
class
SavingsAccount:
public
InterestBearingAccount {
...
// 同上
};
class
CheckingAccount:
public
InterestBearingAccount {
...
// as above
};
存款和支票账户都支付利息, 所以把这个共同行为转移到一个公共基类中; 如果不是所有的银行账户都需要支付利息, 就不能把这行为转移到BankAccount类中哦; 所以引入InterestBearingAccount继承自BankAccount, Saving和Checking的从他继承;
存款和支付账户需要支付信息的行为是通过InterestBearingAccount的纯虚函数creditInterest体现的, 必须在子类中重新定义;
// 好一些,但还不完美
for
(list<BankAccount*>::iterator p = allAccounts.begin(); p != allAccounts.end(); ++p) {
static_cast
<InterestBearingAccount*>(*p)->creditInterest();
}
>尽管依旧包含一个转换, 但代码比之前的健壮, 即使增加InterestBearingAccount的子类, 它还是可以继续正确工作;
为了消除转换, 必须对设计做改变, 1) 可以限制账户列表的类型:
// 银行中所有要支付利息的帐户
list<InterestBearingAccount*> allIBAccounts;
// 可以通过编译且现在将来都可以工作的循环
for
(list<InterestBearingAccount*>::iterator p = allIBAccounts.begin();
p != allIBAccounts.end(); ++p)
{
(*p)->creditInterest();
}
>使用InterestBearingAccount代替BankAccount限制类型;
如果不想用"采用特定列表"的方法, 2) 就让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();
}
Note 虚函数BankAccount::creditInterest提供了空的缺省实现; 它的缺省行为是空操作, 这样也可能会带来难以预见的问题(条款36);
Note creditInterest是一个(隐式的)内联函数, 但同时又是一个虚函数, 内联指令可能会被忽略;(条款33)
"向下转换"可以通过几种方法来消除, 最好的方法是将这种转换用虚函数调用来代替; 它可能对某些类不适用, 所以使这些类的虚函数成为空操作; 第二种方法是加强类型约束, 使得指针的声明类型和你需要的指针类型一致;
Note 值得花费精力去消除向下转换, 因为向下转换代码丑, 容易错, 并且代码难以理解, 升级和维护;
但有些情况下, 还是会不得不执行向下转换;
假如还是开始的情况, allAccounts保存BankAccount指针, creditInterest只是为SavingAccount对象定义, 写一个循环为每个账户计算利息; 假设你不能改动这些类, 不能改变BankAccount, SavingsAccount或allAccounts的定义(如果他们都在某个只读的库中定义) [3rdParty], 这样就只能被迫使用向下转换;
尽管如此, 还是有比上面的原始转换更好的方法: "安全向下转换"; 通过C++的dynamic_cast运算符来实现(M2); 对指针使用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!"
);
}
}
>这种方法不够理想, 但至少可以检测转换失败, 用static_cast无法做到;[基类到子类强转, 调用函数时可能出现类似slicing的越界错误]
Note 对所有转换都失败的情况也要检查; 如上例中的else块;
采用虚函数可以避免这样的检查, 每个虚函数调用必然会被解析为某个函数; 然而一旦进行转换, 虚函数就无法使用; 如果类层次结构中增加了新类型的账户, 但忘记更新上面的代码, 所有的转换就会失败, 所以处理这种可能性很重要;
上例if语句部分, 是dynamic_cast的定义变量方法, 使得代码更简洁; 对psa或pca来说, 只有初始化成功的情况下才会真正被使用; 这样就不需要在条件语句外定义变量(条款32);
老的编译器可能不支持, 那就分开写:
//...
SavingsAccount *psa;
// 传统定义
CheckingAccount *pca;
// 传统定义
if
(psa =
dynamic_cast
<SavingsAccount*>(*p)) {
psa->creditInterest();
}
else
if
(pca =
dynamic_cast
<CheckingAccount*>(*p)) {
pca->creditInterest();
}
psa, pca变量在哪定义并不是很重要; 重点在: 用if-then-else风格的编程来进行向下转换比用虚函数逊色很多, 应该将这种方法保留作为万不得已的方案;
---YC---- Effective C++ 第二版 37)不要重定义非虚函数 38)不要重定义缺省参数值 39)避免向下转换
- [翻译] Effective C++, 3rd Edition, Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)
- 继承的虚函数缺省参数值不要重新定义的原因
- More Effective C++----(4)避免无用的缺省构造函数 & (5)谨慎定义类型转换函数
- 避免对派生的非虚函数进行重定义
- 避免对派生的非虚函数进行重定义
- 不要重新定义继承而来的缺省参数值
- 不要重新定义继承而来的缺省参数值
- 永远不要使用C++的“重定义”
- 绝对不要重新定义继承而来的缺省参数
- Effective C++:条款37:绝不重新定义继承而来的缺省参数值
- 《Effective C++》37: 绝不重新定义继承而来的缺省的参数值
- 读书笔记《Effective C++》条款37:绝不重新定义继承而来的缺省参数值
- C++之绝不重新定义继承而来的缺省参数值(37)---《Effective C++》
- 不要重新定义继承来的非虚函数
- 避免重定义
- 避免对函数中继承得来的默认参数值进行重定义
- 【Skynet】C头文件不要定义函数?
- P2P之我见,关于打洞的学问-------服务器架构
- 如何在 cmd 命令行中查看、修改、删除与添加环境变量
- Attribute和Property的区别
- ArcGIS for Flex求点、线、面的中心点
- Linux PPP实现源码分析-2
- Effective C++ 第二版 37)不要重定义非虚函数 38)不要重定义缺省参数值 39)避免向下转换
- 常用正则表达式
- c与c++符合表差异
- STL中Vector元素的删除
- linux shell编程指南第十四章------环境和shell变量
- 最快,最具可扩展性的文本导入方法 –大数据量加载最佳实践
- MyEclipse中SVN使用步骤
- 35个让人惊讶的 CSS3 动画效果演示
- ORACLE SYSAUX表空间维护之WRH$_SQLTEXT,WRH$_SQL_PLAN