从一开始就让我们简化这次的讨论。你有两类你能够继承的函数:虚函数和非虚函数。然而,重新定义一个非虚函数总是错误的(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 永远不要重新定义继承而来的函数默认参数值的更多相关文章

  1. 读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数

    1. 为什么不要重新定义继承而来的非虚函数——实际论证 假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf.Mf的参数和返回类型并不重要,所以假设它们都是void. ...

  2. Effective C++ -----条款37:绝不重新定义继承而来的缺省参数值

    绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-----你唯一应该覆写的东西-----却是动态绑定.

  3. 读书笔记 effective c++ Item 25 实现一个不抛出异常的swap

    1. swap如此重要 Swap是一个非常有趣的函数,最初作为STL的一部分来介绍,它已然变成了异常安全编程的中流砥柱(Item 29),也是在拷贝中应对自我赋值的一种普通机制(Item 11).Sw ...

  4. 读书笔记 effective c++ Item 34 区分接口继承和实现继承

    看上去最为简单的(public)继承的概念由两个单独部分组成:函数接口的继承和函数模板继承.这两种继承之间的区别同本书介绍部分讨论的函数声明和函数定义之间的区别完全对应. 1. 类函数的三种实现 作为 ...

  5. 读书笔记 effective c++ Item 39 明智而谨慎的使用private继承

    1. private 继承介绍 Item 32表明C++把public继承当作”is-a”关系来对待.考虑一个继承体系,一个类Student public 继承自类Person,如果一个函数的成功调用 ...

  6. 读书笔记 effective c++ Item 49 理解new-handler的行为

    1. new-handler介绍 当操作符new不能满足内存分配请求的时候,它就会抛出异常.很久之前,它会返回一个null指针,一些旧的编译器仍然会这么做.你仍然会看到这种旧行为,但是我会把关于它的讨 ...

  7. 读书笔记 effective c++ Item 52 如果你实现了placement new,你也要实现placement delete

    1. 调用普通版本的operator new抛出异常会发生什么? Placement new和placement delete不是C++动物园中最常遇到的猛兽,所以你不用担心你对它们不熟悉.当你像下面 ...

  8. 读书笔记 effective C++ Item 33 避免隐藏继承而来的名字

    1. 普通作用域中的隐藏 名字实际上和继承没有关系.有关系的是作用域.我们都知道像下面的代码: int x; // global variable void someFunc() { double x ...

  9. 读书笔记 effective c++ Item 7 在多态基类中将析构函数声明为虚析构函数

    1. 继承体系中关于对象释放遇到的问题描述 1.1 手动释放 关于时间记录有很多种方法,因此为不同的计时方法创建一个TimeKeeper基类和一些派生类就再合理不过了: class TimeKeepe ...

随机推荐

  1. 支撑Pinterest日均1000+次试验的A/B测试平台揭秘

    编者按:本文详细介绍了 Pinterest 内部A/B测试平台的搭建过程,对于无论是有技术能力和资源想要自建A/B测试系统的大公司,还是想在业务中引入第三方A/B测试方法和工具的中小公司都极具参考意义 ...

  2. 重新学习WCF

    近来工作不怎么忙,一直在想一个问题,今年刚刚毕业,对于我们这应届生到底应该学习那些技术呢? 面对着现在技术横生,到底哪项是适合自己的呢?自己一直都在迷茫,若有那位大神再次经过,望给出您宝贵的建议. 最 ...

  3. 读书笔记 effective c++ Item3 在任何可能的时候使用 const

    Const可以修饰什么?   Const 关键字是万能的,在类外部,你可以用它修饰全局的或者命名空间范围内的常量,也可以用它来修饰文件,函数和块作用域的静态常量.在类内部,你可以使用它来声明静态或者非 ...

  4. Python开发项目:大型模拟战争游戏(外星人入侵)

    外星人入侵 游戏概述: 现在准备用python开始搞一个大型游戏,模拟未来战争,地球人狙击外星人大战(其实就是小蜜蜂游戏2333),玩家控制一个飞船,用子弹歼灭屏幕上空的外星飞船:项目用到了Pygam ...

  5. 初探 discuz

    测试: vim /etc/hosts       ##ip地址转换 修改windows 的配置文件,写字板打开 vim /usr/local/apache/conf/httpd.conf vim /u ...

  6. mycat

    mycat系列: mycat系列-概述 Cobar的十个秘密之一 Cobar的十个秘密之二 Cobar的十个秘密之三 Cobar的十个秘密之四 Cobar的十个秘密之五 Cobar的十个秘密之六 Co ...

  7. php表单修改数据

    (接前面写的) 第一个页面xiugai.php <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ...

  8. SQL Sever数据库的基本操作和它的建立

    SQL数据库: 1.数据库概述 (1) 用自定义文件格式保存数据的劣势. (2) DBMS(DataBase Management System,数据库管理系统)和数据库,平时谈到"数据库& ...

  9. li点击弹出序号

    <body> <ul> <li>test1</li> <li>test2</li> <li>test3</li ...

  10. HTML入门第二天

    一. URL url:统一资源定位符 组成: 协议://域名:端口号/文件?参数名1=值1&参数名2=值2 例子:http://www.163.com:80/index.html?userna ...