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

相关文章

MySQL常用字符串函数示例和场景介绍

《MySQL常用字符串函数示例和场景介绍》MySQL提供了丰富的字符串函数帮助我们高效地对字符串进行处理、转换和分析,本文我将全面且深入地介绍MySQL常用的字符串函数,并结合具体示例和场景,帮你熟练... 目录一、字符串函数概述1.1 字符串函数的作用1.2 字符串函数分类二、字符串长度与统计函数2.1

python使用try函数详解

《python使用try函数详解》Pythontry语句用于异常处理,支持捕获特定/多种异常、else/final子句确保资源释放,结合with语句自动清理,可自定义异常及嵌套结构,灵活应对错误场景... 目录try 函数的基本语法捕获特定异常捕获多个异常使用 else 子句使用 finally 子句捕获所

C++11范围for初始化列表auto decltype详解

《C++11范围for初始化列表autodecltype详解》C++11引入auto类型推导、decltype类型推断、统一列表初始化、范围for循环及智能指针,提升代码简洁性、类型安全与资源管理效... 目录C++11新特性1. 自动类型推导auto1.1 基本语法2. decltype3. 列表初始化3

C++11右值引用与Lambda表达式的使用

《C++11右值引用与Lambda表达式的使用》C++11引入右值引用,实现移动语义提升性能,支持资源转移与完美转发;同时引入Lambda表达式,简化匿名函数定义,通过捕获列表和参数列表灵活处理变量... 目录C++11新特性右值引用和移动语义左值 / 右值常见的左值和右值移动语义移动构造函数移动复制运算符

C++中detach的作用、使用场景及注意事项

《C++中detach的作用、使用场景及注意事项》关于C++中的detach,它主要涉及多线程编程中的线程管理,理解detach的作用、使用场景以及注意事项,对于写出高效、安全的多线程程序至关重要,下... 目录一、什么是join()?它的作用是什么?类比一下:二、join()的作用总结三、join()怎么

postgresql使用UUID函数的方法

《postgresql使用UUID函数的方法》本文给大家介绍postgresql使用UUID函数的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录PostgreSQL有两种生成uuid的方法。可以先通过sql查看是否已安装扩展函数,和可以安装的扩展函数

MySQL字符串常用函数详解

《MySQL字符串常用函数详解》本文给大家介绍MySQL字符串常用函数,本文结合实例代码给大家介绍的非常详细,对大家学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录mysql字符串常用函数一、获取二、大小写转换三、拼接四、截取五、比较、反转、替换六、去空白、填充MySQL字符串常用函数一、

C++中全局变量和局部变量的区别

《C++中全局变量和局部变量的区别》本文主要介绍了C++中全局变量和局部变量的区别,全局变量和局部变量在作用域和生命周期上有显著的区别,下面就来介绍一下,感兴趣的可以了解一下... 目录一、全局变量定义生命周期存储位置代码示例输出二、局部变量定义生命周期存储位置代码示例输出三、全局变量和局部变量的区别作用域

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

MySql基本查询之表的增删查改+聚合函数案例详解

《MySql基本查询之表的增删查改+聚合函数案例详解》本文详解SQL的CURD操作INSERT用于数据插入(单行/多行及冲突处理),SELECT实现数据检索(列选择、条件过滤、排序分页),UPDATE... 目录一、Create1.1 单行数据 + 全列插入1.2 多行数据 + 指定列插入1.3 插入否则更