读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值
从一开始就让我们简化这次的讨论。你有两类你能够继承的函数:虚函数和非虚函数。然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上。
1. 虚函数是动态绑定的,而默认参数是静态绑定的
在这种情况下,这个条款的验证就相当直接了:虚函数是动态绑定的,而默认参数值是静态绑定的。
这是什么?你说你不堪重负的脑袋已经忘记了动态绑定和静态绑定之间的区别?(为了好记,静态绑定也叫做早绑定(early binding),动态绑定也叫做晚绑定(late binding))。让我们看一下:
一个对象的静态类型是你已经在程序文本中声明的类型,考虑如下的类继承体系:
// a class for geometric shapes
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
// all shapes must offer a function to draw themselves
virtual void draw(ShapeColor color = Red) const = ;
...
}; class Rectangle: public Shape {
public:
// notice the different default parameter value — bad!
virtual void draw(ShapeColor color = Green) const;
...
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
...
};
画成类继承图会是下面这个样子:

现在考虑三个指针:
Shape *ps; // static type = Shape* Shape *pc = new Circle; // static type = Shape* Shape *pr = new Rectangle; // static type = Shape*
在这个例子中,ps,pc和pr都被声明为指向shape的指针,所以它们用Shape作为它们的静态类型。注意无论shape指针真正指向的是什么对象,静态类型都是Shape*。
一个对象的动态类型由指针当前指向的对象类型来决定。也就是,它的动态类型表明了它的行为会是怎样的。看上面的例子,pc的动态类型是Circle*,pr的动态类型是Rectangle*。对于ps,它实际上没有动态类型,因为它还没有引用任何对象。
正如字面意思所表示的,在程序运行时动态类型是可以改变的,特别是通过赋值:
ps = pc; // ps’s dynamic type is now Circle*
ps = pr; // ps’s dynamic type is now Rectangle*
虚函数是动态绑定的,意味着哪个函数被调用是由发出调用的对象的动态类型来决定的:
pc->draw(Shape::Red); // calls Circle::draw(Shape::Red) pr->draw(Shape::Red); // calls Rectangle::draw(Shape::Red)
这些都是旧知识了,我知道你肯定了解虚函数。当你考虑带默认参数值的虚函数时,麻烦出现了,因为虚函数是动态绑定的,但是默认参数是静态绑定的。这意味着你可能会终止一个虚函数的调用,因为函数定义在派生类中却使用了基类中的默认参数:
pr->draw(); // calls Rectangle::draw(Shape::Red)!
在这种情况中,pr的动态类型是Rectangle*,所以Rectangle的虚函数被调用,这也是你所期望的。在Rectangle::draw中,默认参数值是Green。然而因为pr的静态类型是Shape*,这个函数调用的默认参数值是来自于Shape类而不是Rectangle类!最后的结果是这个调用由一个奇怪的也几乎是你意料不到的组合组成:也即是Shape类和Rectangle类中的draw声明混合而成。
Ps,pc和pr都为指针不是造成这个问题的原因。如果它们是引用也同样会出现这个问题。唯一重要的事情是draw是一个虚函数,并且默认参数中的一个在派生类中被重新定义了。
2. C++为什么不对参数进行动态绑定?
为什么C++坚持用一种反常的方式来运行?答案和运行时效率相关。如果一个默认参数是动态绑定的,编译器就需要用一种方法在运行时为虚函数参数确定一个合适的默认值,比起当前在编译期决定这些参数的机制,它更慢更加复杂。做出的决定是更多的考虑了速度和实现的简单性,结果是你可以享受高效的执行速度,但是如果你没有注意到这个条款的建议,你就会很迷惑。
3. 个例讨论——为基类和派生类提供相同的默认参数
这都很好,但是看看如果这么做会发生什么:遵守这个条款的规定并且为基类和派生类函数同时提供默认参数:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
virtual void draw(ShapeColor color = Red) const = ;
...
};
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = Red) const;
...
};
代码重复的问题出现了。更糟糕的是,与代码重复问题便随而来的代码依赖问题:如果Shape中的默认参数被修改了,所有重复这个参数的派生类都需要被修改。否则重新定义继承而来的默认参数值的问题会再度出现。该怎么做?
当你让虚函数按照你的方式来运行时遇到了麻烦,考虑替代设计方法是很明智的,Item 35中介绍了替换虚函数的不同方法。其中的一个是非虚接口用法(NVI idiom):用基类中的public非虚函数调用一个private虚函数,private虚函数可以在派生类中重新被定义。现在,我们用非虚函数指定默认参数,而用虚函数来做实际的工作:
class Shape {
public:
enum ShapeColor { Red, Green, Blue };
void draw(ShapeColor color = Red) const // now non-virtual
{
doDraw(color); // calls a virtual
}
...
private:
virtual void doDraw(ShapeColor color) const = ; // the actual work is
}; // done in this func
class Rectangle: public Shape {
public:
...
private:
virtual void doDraw(ShapeColor color) const; // note lack of a
... // default param val.
};
因为非虚函数应该永远不会在派生类中被重定义(Item 36),这个设计保证draw的color默认参数应该永远是Red。
4. 总结
永远不要重新定义一个继承而来的默认参数值,因为默认参数值是静态绑定的,而虚函数——你应该重新定义的唯一的函数——是动态绑定的。
读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值的更多相关文章
- 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数
1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...
- Effective C++ -----条款37:绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-----你唯一应该覆写的东西-----却是动态绑定.
- 读书笔记 effective c++ Item 25 实现一个不抛出异常的swap
1. swap如此重要 Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11).Sw ...
- 读书笔记 effective c++ Item 34 区分接口继承和实现继承
看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...
- 读书笔记 effective c++ Item 39 明智而谨慎的使用private继承
1. private 继承介绍 Item 32表明C++把public继承当作”is-a”关系来对待.考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用 ...
- 读书笔记 effective c++ Item 49 理解new-handler的行为
1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...
- 读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete
1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面 ...
- 读书笔记 effective C++ Item 33 避免隐藏继承而来的名字
1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...
- 读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数
1. 继承体系中关于对象释放遇到的问题描述 1.1 手动释放 关于时间记录有很多种方法,因此为不同的计时方法创建一个TimeKeeper基类和一些派生类就再合理不过了: class TimeKeepe ...
随机推荐
- SQLSERVER 中实现类似Mysql的 INSERT ON DUPLICATE KEY UPDATE
通过SQLServer创建索引时,有一个IGNORE_DUP_KEY的选项,可以类似实现. IGNORE_DUP_KEY = { ON | OFF } 指定对唯一聚集索引或唯一非聚集索引执行多行插入操 ...
- node.js 的事件机制
昨天到今天, 又看了一边node 的事件模块, 觉得很神奇~ 分享一下 - -> 首先, 补充下对node 的理解: nodeJs 是一个单进程单线程应用程序, 但是通过事件和回调支持并发 ...
- SQL Server-聚焦存储过程性能优化、数据压缩和页压缩提高IO性能(一)
前言 关于SQL Server基础系列尚未结束,还剩下最后一点内容未写,后面会继续.有园友询问我什么时候开始写SQL Server性能系列,估计还得等一段时间,最近工作也比较忙,但是会陆陆续续的更新S ...
- Linux编程之ICMP洪水攻击
我的上一篇文章<Linux编程之PING的实现>里使用ICMP协议实现了PING的程序,ICMP除了实现这么一个PING程序,还有哪些不为人知或者好玩的用途?这里我将介绍ICMP另一个很有 ...
- java批量爬去电影资源
摘要 网上有很多个人站来分享电影资源,其实有时候我们自己也想做这个一个电影站来分享资源.但是这个时候就有一个问题,电影的资源应该从哪里来呢?难道要自己一条条手动去从网络上获取,这样无疑是缓慢而又效率低 ...
- Ubuntu纯字符界面的一些设置
由于Ubuntu的纯字符界面不支持中文显示,所以进行了一些配置,为了更好的显示 1. 把环境语言配置为英文 在用户目录下的".bashrc"文件的结尾处添加以下内容,然后重新登录 ...
- https post
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.N ...
- 在 Windows 上安装 Hadoop 教程(转)
在 Windows 上安装 Hadoop 教程 一见 2010.1.6 www.hadoopor.com/hadoopor@foxmail.com 1. 安装 JDK 不建议只安装 JRE,而是建议直 ...
- JAVA中的数据结构 - 1,红黑树
背景: 在JDK源码中, 有treeMap和JDK8的HashMap都用到了红黑树去存储 红黑树可以看成B树的一种: 二叉树-->搜索二叉树-->平衡搜索二叉树-->B树--> ...
- Python thread local
由于GIL的原因,笔者在日常开发中几乎没有用到python的多线程.如果需要并发,一般使用多进程,对于IO Bound这种情况,使用协程也是不错的注意.但是在python很多的网络库中,都支持多线程, ...