1.关于构造函数的一个违反直觉的行为

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

假设你已经有一个为股票交易建模的类继承体系,它可以买卖股票等。这些交易的可审计性很重要,所以每次交易对象被创建的时候,需要在审计日志中创建一个合适的记录。这看上去是解决问题的合理方法:

 class Transaction { // base class for all

 public: // transactions

 Transaction();

 virtual void logTransaction() const = ; // make type-dependent

 // log entry

 ...

 };

 Transaction::Transaction() // implementation of

 { // base class ctor

 ...

 logTransaction(); // as final action, log this

 } // transaction

 class BuyTransaction: public Transaction { // derived class

 public:

 virtual void logTransaction() const; // how to log trans-

 // actions of this type

 ...

 };

 class SellTransaction: public Transaction { // derived class

 public:

 virtual void logTransaction() const; // how to log trans-

 // actions of this type

 ...

 };

考虑执行下面的代码会发生什么:

 BuyTransaction b;

BuyTransaction的构造函数会被调用,但是在这之前,Transaction的构造函数必须被调用:派生类的基类部分的构建要早于派生类部分。Transaction构造函数的最后一行调用虚函数logTransaction,这个地方会让你感到惊讶。被调用的logTransaction版本是Transaction中的版本而不是BuyTransaction中的版本,即使对象被创建的类型是BuyTransaction.在基类的构造函数中,虚函数永远不会下降到派生类中。相反,对象的行为看上去会像一个基类类型。非正式的说法就是,在基类构建期间,虚函数不再是虚函数

2.这种行为为什么会出现(一)

对于这个违反直觉的行为有一个很好的原因。因为基类构造函数先于派生类构造函数执行,当基类构造函数执行的时候派生类数据成员还没来得及被初始化。如果在基类构造期间虚函数的调用会下降到派生类,派生类函数基本上肯定会引用本地数据成员,但是这些数据成员还没有被初始化呢。这会直达未定义行为和调试到深夜的后果(late-night debugging sessions)。向下调用一个对象的未初始化部分本身就是很危险的,所以c++不让你这么做。

3.这种行为为什么会出现(二)

还有更根本的原因。在派生类对象构建基类部分期间,对象的类型属于基类。不但虚函数会被处理成基类类型,使用运行时类型信息的语言部分(dynamic_cast Item 27和typeid)也会把对象当作基类类型.在我们的例子中,当Transaction构造函数在初始化BuyTransaction对象的基类部分时,对象的类型是Transaction.这就是c++的每个部分是如何处理它的,并且这种处理方法也是合理的:当对象的BuyTransaction部分还没有被初始化,最安全的做法就是当它们不存在一个对象直到派生类构造函数被执行其类型才会变成派生类对象

4.上面的行为析构函数也会出现

理由同样适用于析构函数。一旦一个派生类的析构函数运行完成,就假设对象的派生类数据成员未定义,于是c++当做它们不存在。一进入基类析构函数,对象就会变成一个基类对象,c++的所有部分——虚函数,dynamic_casts等等——都会按基类的方式来处理。

5.如何防止这个行为出现?

在上面的示例代码中,Transaction构造函数直接调用虚函数,很容易看到它违反了这个条款。这个违反是如此容易被发现,一些编译器会发出警告。(其他的则不会,关于warning的讨论见Item53).即使在没有警告的情况下,这个问题在运行时之前很容易显现出来,因为logTransaction函数是Transaction中的纯虚函数。除非它被定义(不太有希望,但是可能,见Item34),否则程序链接会出现问题:链接器将找不到Transaction::logTransaction的定义。

在构造和析构期间对虚函数的调用不总是这么容易能够被发现。如果Transaction有多个构造函数,每个构造函数必须执行相同的工作,防止代码重复的一个好的软件工程是将普通的初始化代码,包含对logTransaction的调用,放到一个私有的非虚初始化函数中,也即是 Init:

 class Transaction {

 public:

 Transaction()

 { init(); } // call to non-virtual...

 virtual void logTransaction() const = ;

 ...

 private:

 void init()

 {

 ...

 logTransaction(); // ...that calls a virtual!

 }

 };

这部分代码和早一点的那个版本从概念上来说是相同的,但是它更加阴险,因为它能够被成功的编译和链接。在这种情况下,因为logTransaction是Transaction的纯虚函数,大多数运行的系统会在调用纯虚函数的时候终止程序(通常会发出一个消息)。然而,如果logTransaction是一个“普通的”虚函数(也就是不是纯虚函数),并且在Transaction中有一个实现,如果这个版本的logTransaction被调用,程序会愉快的执行下去,让你自己去理解为什么创建派生类对象的时候会调用错误的logTransaction版本。防止这个问题的唯一方法是在创建和销毁对象的时候你的构造函数和虚构函数不会去调用虚函数并且它们调用的函数也需要遵守这个约定

6.如何保证调用到继承体系中正确的函数版本

但是你怎么才能够确保每次Transaction继承体系中的对象被创建的时候,能够调用合适的logTransaction版本?这里很清楚,从Transaction中的构造函数中调用这个对象的虚函数是错误的做法。

有不同的方法来处理这个问题。一个方法是将logTransaction变成一个非虚函数,这就需要派生类的构造函数将必要的log信息传递给Transaction构造函数。这时候Transaction构造函数就能够安全的调用非虚的logTransaction,像下面这样:

 class Transaction {

 public:

 explicit Transaction(const std::string& logInfo);

 void logTransaction(const std::string& logInfo) const; // now a non-

 // virtual func

 ...

 };

 Transaction::Transaction(const std::string& logInfo)

 {

 ...

 logTransaction(logInfo); // now a non-

 } // virtual call

 class BuyTransaction: public Transaction {

 public:

 BuyTransaction( parameters )

 : Transaction(createLogString( parameters )) // pass log info

 { ... } // to base class

 ... // constructor

 private:

 static std::string createLogString( parameters );

 };

换句话说,既然你不能够在构造对象期间在基类中使用虚函数向下调用,你可以使用由派生类向上传递必要的构造信息到基类构造函数的方法来进行弥补。

在这个例子中,注意BuyTransaction类中(private)静态函数createLogString的使用。使用一个helper函数来创建传递到基类构造函数的值比在成员初始化列表中提供基类需要的值更加方便(更加易读)。通过将此函数声明成static,就不会有引用BuyTransaction对象未初始化数据成员的危险(static函数只能够操作static数据成员)。这是很重要的,因为数据成员处于未定义状态的事实,就是在基类构造或析构期间调用虚函数不能向下调用的原因。

读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数的更多相关文章

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

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

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

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

  3. EC笔记,第二部分:9.不在构造、析构函数中调用虚函数

    9.不在构造.析构函数中调用虚函数 1.在构造函数和析构函数中调用虚函数会产生什么结果呢? #; } 上述程序会产生什么样的输出呢? 你一定会以为会输出: cls2 make cls2 delete ...

  4. C++ 笔记(二) —— 不要在构造和析构函数中调用虚函数

    ilocker:关注 Android 安全(新手) QQ: 2597294287 class Transaction { //所有交易的 base class public: Transaction( ...

  5. 读书笔记 effective c++ Item 8 不要让异常(exceptions)离开析构函数

    1.为什么c++不喜欢析构函数抛出异常 C++并没有禁止析构函数出现异常,但是它肯定不鼓励这么做.这是有原因的,考虑下面的代码: class Widget { public: ... ~Widget( ...

  6. 读书笔记 effective c++ Item 30 理解内联的里里外外 (大师入场啦)

    最近北京房价蹭蹭猛涨,买了房子的人心花怒放,没买的人心惊肉跳,咬牙切齿,楼主作为北漂无房一族,着实又亚历山大了一把,这些天晚上睡觉总是很难入睡,即使入睡,也是浮梦连篇,即使亚历山大,对C++的热情和追 ...

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

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

  8. 读书笔记 effective c++ Item 51 实现new和delete的时候要遵守约定

    Item 50中解释了在什么情况下你可能想实现自己版本的operator new和operator delete,但是没有解释当你实现的时候需要遵守的约定.遵守这些规则并不是很困难,但是它们其中有一些 ...

  9. 读书笔记 effective c++ Item 27 尽量少使用转型(casting)

    C++设计的规则是用来保证使类型相关的错误不再可能出现.理论上来说,如果你的程序能够很干净的通过编译,它就不会尝试在任何对象上执行任何不安全或无意义的操作.这个保证很有价值,不要轻易放弃它. 不幸的是 ...

随机推荐

  1. 100套新鲜免费的PS笔刷下载

    这篇文章所有的Photoshop笔刷都是免费且高质量的.笔刷总类齐全:有飞鸟.冰块.水.树枝.喷墨.科技元素.皮肤纹理.烟火等等!用它们来加速你的工作流程,提升作品档次吧!”一挪妖娆举动,一刷风情万种 ...

  2. tools_list

    http://files.cnblogs.com/files/yansc/ExportQingtaoImage.rar

  3. 用Django Rest Framework和AngularJS开始你的项目

    Reference: http://blog.csdn.net/seele52/article/details/14105445 译序:虽然本文号称是"hello world式的教程&quo ...

  4. kmp算法理解与记录

    字符串匹配的暴力解法 给定字符串s和p,寻找字符串p在字符串s中出现的位置,暴力解法如下所示: 如果当前字符匹配成功,++i;++j,继续匹配下一字符. 如果s[i]与s[j]匹配失败,令i-=(j- ...

  5. UVa 11456 - Trainsorting

    题目大意:给一个车辆到达车站的序列(按时间先后),可以对车辆进行以下处理:插在队首.插在队尾或者拒绝进站.车站内的车辆必须按照重量大小从大到小排列,问车站内最多能有多少辆车辆? 假设车i是第一个进站, ...

  6. java系列--重载和覆盖小结

    继承中属性的隐藏和方法的覆盖      java中规定,子类用于隐藏的变量可以和父类的访问权限不同,如果访问权限被改变,则以子类的权限为准      java中允许子类的变量与父类变量的类型完全不同, ...

  7. 学习笔记::LCT

    今天听见茹大神20分钟讲完了LCT,10分钟讲完平衡树,5分钟讲完树剖,感觉自己智商还不及他一半... 还有很多不懂:2017/1/15 的理解: access是干什么用的? 不知道,只知道他是用来把 ...

  8. IDEA Show Line Number

    刚开始用IDEA,经常发现右侧没有显示行号,然后去右键选一下,就显现了 一直没有留意这个现象,刚用vim想删几行数据代码,突然发现没有行号了 明明记得刚刚才右键显示了的 好吧,有行号用着比较顺心了.. ...

  9. 关于js中window.location.href,location.href,parent.location.href,top.location.href用法

    "window.location.href"."location.href"是本页面跳转 "parent.location.href"是上一 ...

  10. C,C++,VC++有什么区别

    C语言是一种古老而又经久不衰的计算机程序设计语言,大约诞生于上个世纪60年代.由于它的设计有很多优点,多年以来深受广大程序设计人员的喜爱,并逐渐淘汰了很多其它程序设计语言.我们平时使用的大多数软件都是 ...