读书笔记 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 ...
随机推荐
- JAVA中的基本数类型据
一.基本类型 byte: java中最小的数据类型.1字节/8位.-128(2^7)~127(2^7-1),默认值0. short: 短整型,2字节/16位,取值范围-32768(--2^15)~3 ...
- 为什么Java可以跨平台,而其他语言不行
你好 我是大福 你现在看的是大福笔记 今天复习了Java语言的概述 内容包括Java 语言的历史.语言特点及平台版本 JRE和JDK的区别 这篇文章的主题是总结下对Java语言特点中的跨平台原理. 在 ...
- 《无所不能的JavaScript编程系列:arguments 参数对象》
前言:无所不能的JavaScript JavaScript起源于Netscape公司的LiveScript语言,这是一种基于对象和事件驱动的客户端脚本语言,最初的设计是为了检验HTML表单输入的正确性 ...
- 传输控制协议TCP
TCP主要特点: (1)面向连接 (2)只能是点到点 (3)可靠的传输协议 (4)全双工通信,各自各自的读写缓冲区,应用层吧数据交给发送缓冲区,tcp合适就发送了,接受的时候tcp写入接受缓冲区,应用 ...
- 社交媒体(朋友圈、微博、QQ空间)开发一网打尽,PC端移动端都有!——源码来袭!
一.应用场景 曾几何时,社交媒体已经驻扎到了几乎每个人的生活中.看看你身边的朋友,有几个不玩朋友圈的?就算他不玩朋友圈,那也得玩微博吧.再没有底线,也得玩QQ空间. 不过,作为程序员的我们,没事还是少 ...
- 编写一个飞行棋项目(C#)遇到几个问题:
在写程序中遇到如下问题:如果有人知道,请您一定要指点迷津.小白. 1.在运行暂停功能时,这个暂停功能可以实现,但是无法显示提示信息. case 3: Console.Clear(); Program. ...
- java8-lamba表达式的使用-遁地龙卷风
(-1)前言 学习lamba表达式是十分重要的,你会发现java变的可爱多了. (0)函数式接口 只有一个方法的接口称为函数式接口 JButton jButton = new JButton(&quo ...
- Altium Designer(DXP)小技巧之模块化布局
原创博客转载需注明地址 在我们用Altium Designer进行电路板的绘制的时候经常会遇到模块化布局的问题 就比如电源模块(电源芯片及其外围芯片)放在一起 传感器模块(传感器芯片及其外围芯片)放在 ...
- tornado学习 - TCPServer 实现聊天功能
最近学习tornado框架,其中有很多值得学习的模块,鉴于某位学长的建议,也决定好好看看tornado.tcpserver. 关于TCP协议详细介绍可查阅wiki. 对于TCP服务器,基本的操作tor ...
- hibernate系列笔记(2)---Hibernate的核心API
Hibernate的核心API 一般我们通过hibernate进行操作的时候,都会遵循下面的流程,那么接下来我对每一个步骤进行讲解: 1 public void testInsert() { 2 // ...