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

本文主要是介绍读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

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

 1 class Transaction { // base class for all2 3 public: // transactions4 5 Transaction();6 7 virtual void logTransaction() const = 0; // make type-dependent8 9 // log entry
10 
11 ...
12 
13 };
14 
15 Transaction::Transaction() // implementation of
16 
17 { // base class ctor
18 
19 ...
20 
21 logTransaction(); // as final action, log this
22 
23 } // transaction
24 
25 class BuyTransaction: public Transaction { // derived class
26 
27 public:
28 
29 virtual void logTransaction() const; // how to log trans-
30 
31 // actions of this type
32 
33 ...
34 
35 };
36 
37 class SellTransaction: public Transaction { // derived class
38 
39 public:
40 
41 virtual void logTransaction() const; // how to log trans-
42 
43 // actions of this type
44 
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 func
10 
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 call
24 
25 class BuyTransaction: public Transaction {
26 
27 public:
28 
29 BuyTransaction( parameters )
30 
31 : Transaction(createLogString( parameters )) // pass log info
32 
33 { ... } // to base class
34 
35 ... // constructor
36 
37 private:
38 
39 static std::string createLogString( parameters );
40 
41 };

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

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

这篇关于读书笔记 effective c++ Item 9 绝不要在构造函数或者析构函数中调用虚函数的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/882658

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

hdu1171(母函数或多重背包)

题意:把物品分成两份,使得价值最接近 可以用背包,或者是母函数来解,母函数(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v)(1 + x^v+x^2v+.....+x^num*v) 其中指数为价值,每一项的数目为(该物品数+1)个 代码如下: #include<iostream>#include<algorithm>

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现