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. Hive 分组问题

    group by 中出现的字段不能再select 后面单独显示,必须配合函数使用 上面中的 ' group by id 总结: Hive不允许直接访问非group by字段: 对于非group by字 ...

  2. [Maven]Maven 那点事儿

    0. 前言 Jason Van Zyl,在 Java 十大风云人物排行榜上或许会看到他. 这兄弟是干嘛的? 他就是 Maven 的创始人,人们都尊称他为"Maven 他爸". 毋庸 ...

  3. Unity3D面试题汇总

    1.请描述游戏动画有哪几种,以及其原理. 2.alpha blend 工作原理 3.写光照计算中的diffuse的计算公式 4.lod是什么,优缺点是什么 5.两种阴影判断的方法工作原理 6.MipM ...

  4. ORM之PetaPoco入门(一)--Petapoco简介

    1. ORM概括 1.1. ORM简介 ORM 对象-关系映射(Object/Relation Mapping,简称ORM),是随着面向对象的软件开发方法发展而产生的.面向对象的开发方法是当今企业级应 ...

  5. 内存管理——Cocos2d-x学习历程(五)

    Cocos2d-x采用了引用计数与自动回收的内存管理机制. 1.每个对象包含一个用来控制生命周期的引用计数器,它就是CCObject的成员变量m_u- Reference.我们可以通过retainCo ...

  6. cassandra 3.x官方文档(5)---探测器

    写在前面 cassandra3.x官方文档的非官方翻译.翻译内容水平全依赖本人英文水平和对cassandra的理解.所以强烈建议阅读英文版cassandra 3.x 官方文档.此文档一半是翻译,一半是 ...

  7. C#生成JOSN字符串

    C#如何生成JSON字符串?(序列化对象)   第一章:C#如何拿到从http上返回JSON数据? 第二章:C#如何解析JSON数据?(反序列化对象) 第三章:C#如何生成JSON字符串?(序列化对象 ...

  8. leetcode第一刷_Merge Intervals

    看到这个题我就伤心啊,去微软面试的时候,第一个面试官让我做的题目就是实现集合的交操作,这个集合中的元素就像这里的interval一样.是一段一段的.当时写的那叫一个慘不忍睹.最后果然被拒掉了. .好好 ...

  9. [OC] @property时,copy、strong、weak、assign的区别

    @property(copy,nonatomic)NSMutableString*copyStr; @property(strong,nonatomic)NSMutableString*strongS ...

  10. 4A Watermelon

    A. Watermelon time limit per test 1 second memory limit per test 64 megabytes input standard input o ...