读书笔记 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

相关文章

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

C++中RAII资源获取即初始化

《C++中RAII资源获取即初始化》RAII通过构造/析构自动管理资源生命周期,确保安全释放,本文就来介绍一下C++中的RAII技术及其应用,具有一定的参考价值,感兴趣的可以了解一下... 目录一、核心原理与机制二、标准库中的RAII实现三、自定义RAII类设计原则四、常见应用场景1. 内存管理2. 文件操

C++中零拷贝的多种实现方式

《C++中零拷贝的多种实现方式》本文主要介绍了C++中零拷贝的实现示例,旨在在减少数据在内存中的不必要复制,从而提高程序性能、降低内存使用并减少CPU消耗,零拷贝技术通过多种方式实现,下面就来了解一下... 目录一、C++中零拷贝技术的核心概念二、std::string_view 简介三、std::stri

C++高效内存池实现减少动态分配开销的解决方案

《C++高效内存池实现减少动态分配开销的解决方案》C++动态内存分配存在系统调用开销、碎片化和锁竞争等性能问题,内存池通过预分配、分块管理和缓存复用解决这些问题,下面就来了解一下... 目录一、C++内存分配的性能挑战二、内存池技术的核心原理三、主流内存池实现:TCMalloc与Jemalloc1. TCM

Python中help()和dir()函数的使用

《Python中help()和dir()函数的使用》我们经常需要查看某个对象(如模块、类、函数等)的属性和方法,Python提供了两个内置函数help()和dir(),它们可以帮助我们快速了解代... 目录1. 引言2. help() 函数2.1 作用2.2 使用方法2.3 示例(1) 查看内置函数的帮助(

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

C++作用域和标识符查找规则详解

《C++作用域和标识符查找规则详解》在C++中,作用域(Scope)和标识符查找(IdentifierLookup)是理解代码行为的重要概念,本文将详细介绍这些规则,并通过实例来说明它们的工作原理,需... 目录作用域标识符查找规则1. 普通查找(Ordinary Lookup)2. 限定查找(Qualif

Java调用C#动态库的三种方法详解

《Java调用C#动态库的三种方法详解》在这个多语言编程的时代,Java和C#就像两位才华横溢的舞者,各自在不同的舞台上展现着独特的魅力,然而,当它们携手合作时,又会碰撞出怎样绚丽的火花呢?今天,我们... 目录方法1:C++/CLI搭建桥梁——Java ↔ C# 的“翻译官”步骤1:创建C#类库(.NET

Python中bisect_left 函数实现高效插入与有序列表管理

《Python中bisect_left函数实现高效插入与有序列表管理》Python的bisect_left函数通过二分查找高效定位有序列表插入位置,与bisect_right的区别在于处理重复元素时... 目录一、bisect_left 基本介绍1.1 函数定义1.2 核心功能二、bisect_left 与

java中BigDecimal里面的subtract函数介绍及实现方法

《java中BigDecimal里面的subtract函数介绍及实现方法》在Java中实现减法操作需要根据数据类型选择不同方法,主要分为数值型减法和字符串减法两种场景,本文给大家介绍java中BigD... 目录Java中BigDecimal里面的subtract函数的意思?一、数值型减法(高精度计算)1.