1. 为什么不要重新定义继承而来的非虚函数——实际论证

假设我告诉你一个类D public继承类B,在类B中定义了一个public成员函数mf。Mf的参数和返回类型并不重要,所以假设它们都是void。实现如下:

 class B {
public:
void mf();
...
};
lass D: public B { ... }

我们不需要了解B,D或者mf的任何细节,考虑一个类型D的对象x,

 D x;                               // x is an object of type D

你会感到很吃惊,如果下面的语句:

 B *pB = &x;                               // get pointer to x

 pB->mf();                                 // call mf through pointer

同下面的语句行为不一样

 D *pD = &x;                              // get pointer to x

 pD->mf();                                // call mf through pointer

因为两种情况中你都触发了对象x的成员函数mf。因为两种情况使用了相同的对象和相同的函数,行为就应该是相同的,不是么?

应该是这样。但是可能也不是这样。特别的,如果mf是非虚函数并且D定义了自己的mf版本的情况下,上面的行为会不一样:

 class D: public B {
public:
void mf(); // hides B::mf; see Item 33 ... };
pB->mf(); // calls B::mf pD->mf(); // calls D::mf

这种两面性行为的原因是像B::mf和D::mf这样的非虚函数都是静态绑定的(statically bound Item
37)。这意味着因为pB被声明为指向B的指针,通过pB触发的非虚函数都会是定义在类B上的函数,即使pB指向的是B的派生类对象,也就是这个例子中所实现的那样。

而虚函数是动态绑定的(dynamically bound
Item 37),所以它们不会有上面的问题。如果mf是一个虚函数,无论通过pB或者pD对mf进行调用都会触发D::mf,因为pB或者pD真正指向的是类型D的对象。

如果你实现一个类D并且重新定义继承自类B的非虚函数mf,D对象也会表现出不一致的行为。特别的,通过D对象调用mf函数的行为可能会像B也可能像D,而对象本身并不是决定因素,决定因素是指向D对象的指针类型。引用所展示出来的行为会同指针一样。

2. 为什么不要重新定义继承而来的非虚函数——理论论证

上面只是实际论证。我知道你真正想了解的是“不要重新定义继承而来的非虚函数”的理论证明。看下面的分析:

Item 32解释了pulibc继承意味着”is-a”,Item34中描述了为什么在一个类中声明一个非虚函数就是为这个类建立了一个特化上的不变性(invariant over
specialization)。如果你将这些观察应用到类B或者D以及非虚成员函数B::mf上,你会发现:

  • 应用在类B上的任何事情同样能够应用在类D上,因为每个D对象都是一个B对象
  • 派生自类B的类同时继承了mf的接口和实现,因为mf是B中的非虚函数。

现在,如果D重新定义了mf,你的设计就会出现矛盾。如果D真的想实现一个不同于B的mf,并且如果每个B对象——无论怎么特化——真的必须为mf使用B的实现,每个D对象都是一个B对象就不是真的了。在这种情况下,D不应该public继承自B。从另外一个方面,如果D真的必须public继承自B,并且D真的需要实现一个不同于B的mf,那么mf为B反应出来的特化上的不变性就不再为真。在这种情况下,mf应该是virtual的。最后,如果每个D真的是一个B,并且如果mf真的为B反应出了特化上的不变性,那么D真的不需要重新定义mf,也不应该尝试去这么做。

无论哪个观点,结论都相同,禁止重新定义一个继承而来的非虚函数。

3. 你应该对此条款似曾相识

如果阅读这个条款的时候给你一种似曾相识的感觉,可能是因为你已经读过Item7了,这个条款解释了为什么多态基类中的虚函数应该为virtual的。如果你违反了Item7中的条款(也就是你在多态基类中声明一个非虚析构函数),你也会违反这个条款,因为派生类总是会重新定义继承而来的非虚函数:基类的析构函数。这对于没有定义析构函数的派生类来说也为真,因为正如Item 5中所解释的,如果你自己没有声明析构函数,编译器会为你自动生成一个。从本质上来说,Item7只是这个条款的一个特例而已,尽管它足够重要到单独成为一个条款。

4. 总结

    • 永远不要重新定义一个继承而来的非虚函数。

读书笔记 effective c++ Item 36 永远不要重新定义继承而来的非虚函数的更多相关文章

  1. 读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值

    从一开始就让我们简化这次的讨论.你有两类你能够继承的函数:虚函数和非虚函数.然而,重新定义一个非虚函数总是错误的(Item 36),所以我们可以安全的把这个条款的讨论限定在继承带默认参数值的虚函数上. ...

  2. 读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数

    关于构造函数的一个违反直觉的行为 我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数,因为这些调用的结果会和你想的不一样.如果你同时是一个java或者c#程序员,那么请着重注意这个条款,因为 ...

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

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

  4. 《effective C++》:条款36——绝不重新定义继承而来的非虚函数

    (1)当派生类中重写了基类的非虚函数时,这个时候这个函数发生的是静态绑定 下面中的代码中: 定义一个基类B,基类定义了函数fcm,fcm是非虚的函数. 定义一个派生类D,派生类重新定义了fcm. 当用 ...

  5. 读书笔记 effective c++ Item 43 了解如何访问模板化基类中的名字

    1. 问题的引入——派生类不会发现模板基类中的名字 假设我们需要写一个应用,使用它可以为不同的公司发送消息.消息可以以加密或者明文(未加密)的方式被发送.如果在编译阶段我们有足够的信息来确定哪个信息会 ...

  6. 读书笔记 effective c++ Item 35 考虑虚函数的替代者

    1. 突破思维——不要将思维限定在面向对象方法上 你正在制作一个视频游戏,你正在为游戏中的人物设计一个类继承体系.你的游戏处在农耕时代,人类很容易受伤或者说健康度降低.因此你决定为其提供一个成员函数, ...

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

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

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

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

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

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

随机推荐

  1. NAT详解

    1.为什么出现了NAT? IP地址只有32位,最多只有42.9亿个地址,还要去掉保留地址.组播地址,能用的地址只有36亿左右,但是当下有数以万亿的主机,没有这么多IP地址怎么办,后面有了IPv6,但是 ...

  2. arcpy.mapping-认识arcpy.mapping

    arcpy.mapping-认识arcpy.mapping by 李远祥 ArcMap提供了强大的地图制图功能,在实际的工作中,只要涉及到专题地图和地图册的制作,难免会遇到非常多的出图工作和地图图面元 ...

  3. 计算机 计算机语言 常见的DOS命令

    你好 我是大福 你现在看的是大福笔记 我jie的女儿 8年级 在QQ上问我寒假作业 0度的水和0度的冰哪个更冷 什么? 0度水 0度冰这温度不是一样的么? 不可能 肯定没这么简单 她问的意思是不是手放 ...

  4. 学习笔记--jQuery基础

    学习笔记也算总结知识点,有些示例可能没有具体到元素,直接给出的语法.大家有基本理解在看可能会好些. jQuery使用前提,需要安装jQuery库 jQuery 库是一个 JavaScript 文件,您 ...

  5. MongoDB学习总结(五) —— 安全认证

    作为数据库软件,我们要确保数据的安全,不是谁都可以访问的,所以mongodb也像其他的数据库软件一样可以采用用户验证的方法, mongodb 3.0之前的版本提供了addUser方法向不同的数据库添加 ...

  6. 1.使用SignalR实现页面即时刷新(服务端主动推送)

    模块功能说明: 实现技术:sqlserver,MVC,WebAPI,ADO.NET,SignalR(服务器主动推送) 特殊车辆管理--->移动客户端采集数据存入数据库---->只要数据库数 ...

  7. Javascript—①你好,世界!

    新手Perfect教程之Javascript教程①-你好,世界! 前言:不知道Javascript是什么东东的自行度娘或google一下 Javascript在html<head>和< ...

  8. mysql优化---订单查询优化:异步分页处理

    订单分页查询: 老的代码是顺序执行查询数据和计算总记录数,但是如果条件复杂的话(比如关联子表)查询的时间要超过20s种 public static PagedList<Map<String ...

  9. jMeter入门实例

    在大家开始看的时候先声明这篇文章不是我写的我只是照着他讲的安装好了,我觉得很有用就想保留下来.分享给大家. 人们对从认识事物都有一个具体到抽象的过程,学习Jmeter也不例外,通过一个实例来进行学习, ...

  10. 关于c语言中栈和堆释放的问题

    #include<iostream> #include<string> using namespace std; int main() { string st; cin> ...