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. 【4Large-Style】前端框架设计——Button 的设计思路

    Button 的设计 Button 作为基本的 Web 元素,看似简单,却需要非常用心的设计,因为 Button 作为按钮,是具有多个不同的状态,每种状态都基本上需要进行一些特殊的优化设计,以让组件更 ...

  2. autotools归纳

    最近接触到许多linux项目,其编译都是使用的autotools. autotools是一个自动化的编译工具.个人理解它的最主要功能就是生成Makefile. 因为直接写Makefiel,其依赖关系还 ...

  3. MySQL千万级多表关联SQL语句调优

    本文不涉及复杂的底层数据结构,通过explain解释SQL,并根据可能出现的情况,来做具体的优化.   需要优化的查询:使用explain      出现了Using temporary:       ...

  4. AR_Demon(使用vuforia平台提供的钥匙跟后台,实现相机拍图片读取模型以及视频的功能)

    1.https://developer.vuforia.com注册账号. 2.分别添加License Manager(连接钥匙),Target Manager(图片后台下载). 3.下载vuforia ...

  5. pom.xml配置详解

    <!--可以免费转载,转载时请注明出处  http://pengqb.iteye.com .--><project xmlns="http://maven.apache.o ...

  6. int与integer的区别(基本数据类型与引用数据类型)

    一.先说说int与integer的区别 int 是基本数据类型,默认值为0,不需要进行实例化 integer 是引用数据类型,是int的封装类型,默认值为null,创建该类型需要进行实例化. 基本数据 ...

  7. UE4 RHI与Render模块简解

    UE4中的RHI指的是Render hardware interface,作用像Ogre里的RenderSystem,针对Dx11,Dx12,Opengl等等平台抽象出相同的接口,我们能方便能使用相同 ...

  8. 利用AForge.NET 调用电脑摄像头进行拍照

    当然了,你需要去官网下载类库,http://www.aforgenet.com/ 调用本机摄像头常用的组件: AForge AForge.Controls AForge.Imaging AForge. ...

  9. Linux-7.2+LNMP+zabbix-3.2.1

    LNMP+zabbix-3.2.1 一.zabbix服务端部署 1.解压 yum –y install bzip2 tar -xf nginx-1.10.1.tar.gz tar -xf php-5. ...

  10. XHTML清单

    1.无序清单 <ul> <li>...</li> <li>...</li> <li>...</li> </ul ...