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

小编 2026-06-22 阅读:166 评论:0
1.关于构造函数的一个违反直觉的行为我会以重复标题开始:你不应该在构造或者析构的过程中调用虚函数...

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

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

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

 1 class Transaction { // base class for all 2  3 public: // transactions 4  5 Transaction(); 6  7 virtual void logTransaction() const = 0; // make type-dependent 8  9 // log entry10 11 ...12 13 };14 15 Transaction::Transaction() // implementation of16 17 { // base class ctor18 19 ...20 21 logTransaction(); // as final action, log this22 23 } // transaction24 25 class BuyTransaction: public Transaction { // derived class26 27 public:28 29 virtual void logTransaction() const; // how to log trans-30 31 // actions of this type32 33 ...34 35 };36 37 class SellTransaction: public Transaction { // derived class38 39 public:40 41 virtual void logTransaction() const; // how to log trans-42 43 // actions of this type44 45 ...46 47 };

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

1 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:

 1 class Transaction { 2  3 public: 4  5 Transaction() 6  7 { init(); } // call to non-virtual... 8  9 virtual void logTransaction() const = 0;10 11 ...12 13 private:14 15 void init()16 17 {18 19 ...20 21 logTransaction(); // ...that calls a virtual!22 23 }24 25 };

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

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

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

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

 1 class Transaction { 2  3 public: 4  5 explicit Transaction(const std::string& logInfo); 6  7 void logTransaction(const std::string& logInfo) const; // now a non- 8  9 // virtual func10 11 ...12 13 };14 15 Transaction::Transaction(const std::string& logInfo)16 17 {18 19 ...20 21 logTransaction(logInfo); // now a non-22 23 } // virtual call24 25 class BuyTransaction: public Transaction {26 27 public:28 29 BuyTransaction( parameters )30 31 : Transaction(createLogString( parameters )) // pass log info32 33 { ... } // to base class34 35 ... // constructor36 37 private:38 39 static std::string createLogString( parameters );40 41 };

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

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


作者:HarlanC

博客地址:http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

版权声明

本文仅代表作者观点,不代表百度立场。
本文系作者授权百度百家发表,未经许可,不得转载。

热门文章
  • 机房智能化温湿度解决方式之POE供电以太网温湿度传感器

    机房智能化温湿度解决方式之POE供电以太网温湿度传感器
    机房智能化温湿度解决方式之POE供电以太网温湿度传感器 北京盈创力和电子科技有限公司 智能型TCP网口温湿度记录仪 北京IP网络温湿度记录仪厂家,北京盈创力和 北京智能型TCP网口温湿度记录仪IP网络温湿度记录仪是一种新型的基于TCP/IP协议双绞线以太网标准温湿度采集模块,利用它可以实现现场温度值、相对湿度值的采集,同时利用其自身的RJ45通信接口可以方便地和机房监控主机或交换机集线器进行联网。 工作于-40℃~85℃工业级带...
  • Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering

    Sequential Monte Carlo Methods (SMC) 序列蒙特卡洛/粒子滤波/Bootstrap Filtering
    Problem Statement 我们考虑一个具有马尔可夫性质、非线性、非高斯的状态空间模型(State Space Model):对于一个时间序列上的观测结果{yt,t∈N}\\{ y_t , t \\in N \\}{yt​,t∈N},我们认为每个观测结果yty_tyt​的生成依赖于一个无法直接观察的隐变量xt∈{xt,t∈N}x_t \\in \\{x_t , t \\in N \\}xt​∈{xt​,t∈N},即:p(...
  • HTTP状态保持的原理

    HTTP状态保持的原理
    a)在用户登录之后,浏览器返回响应的时候会在响应中添加上cookieb)浏览器接收到cookie之后会自动保存c)当用户再次请求同一服务器中的其他网页的时候,浏览器会自动带上之前保存的cookied)服务接收到请求之后可以请 request 对象中取到cookie 判断当前用户是否登录  Http是无状态的,就是连接时数据互通,关闭后...
  • Hive 系统函数及示例

    Hive 系统函数及示例
    查看所有系统函数 show functions; 函数分类 内置函数【系统函数】 数学函数: floor、round、ceil、cos、log2等 字符串函数: length、reverse、trim、lower、get_json_object、repeat等 收集函数: size 转换函数: cast 日期函数: year、month、datediff、date、date_add等 条件函数: coalesce、case…w...
  • CSRF的原理和防范措施

    CSRF的原理和防范措施
    a)攻击原理:i.用户C访问正常网站A时进行登录,浏览器保存A的cookieii.用户C再访问攻击网站B,网站B上有某个隐藏的链接或者图片标签会自动请求网站A的URL地址,例如表单提交,传指定的参数iii.而攻击网站B在访问网站A的时候,浏览器会自动带上网站A的cookieiv.所以网站A在接收到请求之后可判断当前用户是登录状态,所以...
标签列表