Effective C++条款09——绝不在构造和析构过程中调用virtual 函数(构造/析构/赋值运算)

news2024/10/7 18:22:44

本条款开始前我要先阐述重点:你不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预想的结果,就算有你也不会高兴。如果你同时也是一位Java或C#程序员,请更加注意本条款,因为这是CH+与它们不相同的一个地方。

假设你有个class 继承体系,用来塑模股市交易如买进、卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志( audit log)中也需要创建一笔适当记录。下面是一个看起来颇为合理的做法:

class Transaction
{
public:
    Transaction();                           // 做出一份因类型不同而
    virtual void logTransaction() const = 0; // 不同的日志记录
    // ...
};

Transaction::Transaction()                   // 基类构造函数实现
{ 
  // ...
    logTransaction();                        // 最后动作是记这笔交易
}

class BuyTransaction : public Transaction    // 子类
{ 
public:
    virtual void logTransaction() const;     // 日志记录
    // ...
};

class SellTransaction : public Transaction   // 子类
{ 
public:
    virtual void logTransaction() const; // 日志记录
    // ...
};

现在,当以下这行被执行,会发生什么事:

BuyTransaction b;

结果

无疑地会有一个 BuyTransaction构造函数被调用,但首先Transaction构造函数一定会更早被调用;是的,derived class对象内的 base class成分会在derived class自身成分被构造之前先构造妥当。Transaction构造函数的最后一行调用virtual函数logTransaction,这正是引发惊奇的起点。这时候被调用的 logTransaction是Transaction内的版本,不是BuyTransaction内的版本——即使目前即将建立的对象类型是 BuyTransaction。是的,base class 构造期间 virtual函数绝不会下降到derived classes阶层。取而代之的是,对象的作为就像隶属base类型一样。非正式的说法或许比较传神:在 base class构造期间,virtual函数不是virtual函数。

这一似乎反直觉的行为有个好理由。由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes阶层,要知道derived class的函数几乎必然取用local成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会串的直达车票。“要求使用对象内部尚未初始化的成分”是危险的代名词,所以C++不让你走这条路。

透彻

其实还有比上述理由更根本的原因:在derived class对象的base class构造期间对象的类型是base class而不是 derived class。

话没读懂

不只virtual函数会被编译器解析至(resolve to) base class,若使用运行期类型信息( runtime type information,例如dynamic_cast(见条款27)和typeid),也会把对象视为 base class类型。本例之中,当Transaction构造函数正执行起来打算初始化“BuyTransaction对象内的base class成分”时,该对象的类型是Transaction。那是每一个C++次成分(见条款1〉的态度,而这样的对待是合理的:这个对象内的“BuyTransaction专属成分”尚未被初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class构造函数开始执行前不会成为一个derived class对象。

相同道理也适用于析构函数。一旦 derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++视它们仿佛不再存在。进入 baseclass析构函数后对象就成为一个base class对象,而C++的任何部分包括virtual函数、dynamic_casts 等等也就那么看待它。

在上述示例中,Transaction构造函数直接调用一个 virtual函数,这很明显而且容易看出违反本条款。由于它很容易被看出来,某些编译器会为此发出一个警告信息(某些则否,见条款53对警告信息的讨论)。即使没有这样的警告,这个问题在执行前也几乎肯定会变得显而易见,因为logTransaction函数在Transaction内是个pure virtual。除非它被定义(不太有希望,但是有可能,见条款34)否则程序无法连接,因为连接器找不到必要的Transaction:: logTransaction实现代码。

但是侦测“构造函数或析构函数运行期间是否调用 virtual函数”并不总是这般轻松。如果Transaction有多个构造函数,每个都需执行某些相同工作,那么避免代码重复的一个优秀做法是把共同的初始化代码(其中包括对logTransacttion的调用)放进一个初始化函数如init内:

class Transaction
{
public:
    Transaction() {
        init();
    }                      
    virtual void logTransaction() const = 0; 
    // ...

private:
    void init() {
        // ...
        logTransaction();               // 这里调用virtual
    }
};

这段代码概念上和稍早版本相同,但它比较潜藏并且暗中为害,因为它通常不会引发任何编译器和连接器的抱怨。此时由于logTransaction是Transaction内的一个pure virtual函数,当pure virtual函数被调用,大多执行系统会中止程序(通常会对此结果发出一个信息)。然而如果 logTransaction是个正常的(也就是impure)virtual函数并在Transaction内带有一份实现代码,该版本就会被调用,而程序也就会兴高采烈地继续向前行,留下你百思不解为什么建立一个derived class对象时会调用错误版本的logTransaction。唯一能够避免此问题的做法就是:确定你的构造函数和析构函数都没有(在对象被创建和被销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束。

但你如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢﹖很显然,在Transaction构造函数(s)内对着对象调用virtual函数是一种错误做法。

其他方案可以解决这个问题。一种做法是在 class Transaction内将logTransaction函数改为non-virtual,然后要求derived class构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtuallogTransaction。像这样:

class Transaction {
public:
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;    // 如今是个非虚函数
    // ...
};

Transaction::Transaction(const std:: string& logInfo) {
    // ...
    logTransaction(logInfo);                                // 非虚调用
}


class BuyTransaction: public Transaction {
public:
    // 将log信息传给基类构造函数
    BuyTranscation(parameters) :Transaction(createLogString(parameters)) {}
    // ...

private:
    static std::string createLogString(parameters);
};

换句话说由于你无法使用virtual函数从base classes向下调用,在构造期间,你可以藉由“令derived classes将必要的构造信息向上传递至base class构造函数”替换之而加以弥补。

请注意本例之 BuyTransaction内的private static函数createLogString 的运用。是的,比起在成员初值列(member initialization list)内给予base class所需数据,利用辅助函数创建一个值传给base class构造函数往往比较方便(也比较可读)。令此函数为static,也就不可能意外指向“初期未成熟之BuyTransaction对象内尚未初始化的成员变量”。这很重要,正是因为“那些成员变量处于未定义状态”,所以“在base class构造和析构期间调用的virtual函数不可下降至derived classes”。

请记住

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那层)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/912746.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

斗鱼财报盈利的背后:左手艳舞、右手擦边

本月14日,直播平台斗鱼发布了其第二季度财报,面对“看起来还不错的数据”,其对外着重强调了“连续两个季度实现盈利”,并称“斗鱼收入结构持续优化”“斗鱼盈利能力提升”“斗鱼稳健增长可期”“督导提升了内容审核能力”。 财报…

【C++STL基础入门】深入浅出string类查找字串、返回字串和交换操作

文章目录 前言一、查找字串二、返回字串三、交换字串四、运算符重载总结 前言 本STL使用VS2022C20版本 C标准库(Standard Template Library,简称STL)是C中非常强大和常用的一组容器、算法和函数模板,它能大大简化程序开发和提高…

MySQL高级篇——MySQL架构篇2(MySQL的数据目录)

目录 1 MySQL8的主要目录结构1.1 数据库文件的存放路径1.2 相关命令目录1.3 配置文件目录 2 数据库和文件系统的关系2.1 查看默认数据库2.2 数据库在文件系统中的表示2.3.1 InnoDB存储引擎模式2.3.2 MyISAM存储引擎模式 2.4 总结2.5 视图在文件系统中的表示2.6 其他的文件 1 My…

【Linux】vim编辑器

这一趴我们要学习vim编辑器,知道vim是什么、掌握它的3种模式以及学会其常见操作。 目录 Ⅰ. 引入 Ⅱ. 什么是vim? Ⅲ. 3种模式 Ⅳ. 常见操作 命令模式下 复制粘贴组 撤销组 光标定位组 光标移动组 剪切组 切换、替换组 删除组 底行模式下 …

AI+游戏线下沙龙活动暨COC上海城市开发者社区8月活动

引言 近年来,随着人工智能技术的不断发展和游戏开发技术的不断更新,越来越多的游戏公司开始将人工智能技术应用于游戏领域,以提高开发效率、降低成本,实现游戏玩家更好的游戏体验。为了探讨AI游戏的技术实践经验,近日在…

Spring Cloud Alibaba笔记

😀😀😀创作不易,各位看官点赞收藏. 文章目录 Spring Cloud Alibaba 笔记1、Nacos 服务注册和配置中心1.1、Nacos 之下载启动1.2、Nacos 之注册中心1.3、Nacos 之服务发现1.4、Nacos 之配置中心1.5、Nacos 之分类配置1.6、Nacos 之…

【OpenGauss源码学习 —— 执行算子(Result 算子)】

执行算子(Result 算子) 控制算子Result 算子ExecInitResult 函数ResultState 结构体ExecInitResultTupleSlot 函数ExecAllocTableSlot函数 ExecResult 函数TupleTableSlot 结构体ExecProcNode 函数ExecProcNodeByType 函数ExecProject 函数 ExecEndResul…

docker版jxTMS使用指南:使用jxTMS提供数据

本文讲解了如何jxTMS的数据访问框架,整个系列的文章请查看:docker版jxTMS使用指南:4.4版升级内容 docker版本的使用,请查看:docker版jxTMS使用指南 4.0版jxTMS的说明,请查看:4.0版升级内容 4…

韶音的骨传导耳机怎么样,韶音骨传导耳机是真的骨传导吗

韶音骨传导耳机最为受瞩目的是OpenRun Pro,在发声单元位置上采用了开孔的处理,佩戴上耳的时候发声单元可以贴合耳道,在低频延伸性,但在中高频的时候整体会出现震感,纤细的耳挂在佩戴的时候是有着不错的舒适度的&#x…

Java SpringBoot+Vue 的班级综合测评管理系统的设计与实现(2.0 版本)

文章目录 1. 简介2. 技术栈 3. 需求分析用户需求分析功能需求分析系统性能需求分析 4系统总体设计与实现4.1总体设计 5 系统功能的详细设计与实现5.1 管理员功能模块5.2学生功能模块5.3教师功能模块 源码下载地址 1. 简介 传统的班级综合测评管理系统,一开始都是手工…

Django实现音乐网站 ⒀

使用Python Django框架制作一个音乐网站, 本篇主要是推荐页-推荐排行榜、推荐歌手功能开发。 目录 推荐页开发 推荐排行榜 单曲表增加播放量 表模型增加播放量字段 执行表操作 模板中显示外键对应值 表模型外键设置 获取外键对应模型值 推荐排行榜视图 推…

基于web的鲜花商城系统java jsp网上购物超市mysql源代码

本项目为前几天收费帮学妹做的一个项目,Java EE JSP项目,在工作环境中基本使用不到,但是很多学校把这个当作编程入门的项目来做,故分享出本项目供初学者参考。 一、项目描述 基于web的鲜花商城系统 系统有2权限:前台…

HCIA---访问控制列表

文章目录 目录 前言 一.ACL简介: 二.ACL工作原理 ACL组成: ​编辑 规则编号: ACL匹配规则: 总结 前言 一.ACL简介: ACL全称为Access Control List,即访问控制表,是一种用于控制网络资源访问…

微软韦青:滑向冰球将要到达的位置 | 科创人数智思维私董会第9期回顾

2023年8月5日,由科创人、北航投资联手创办的科创人数智思维私董会第9期圆满举行。 微软(中国)首席技术官韦青担任本次活动引导嘉宾,近20位来自传统企业、科创企业、投资机构的CEO、技术决策者及领域专家,围绕新人-机时…

【技术篇】• 饮用水除硝酸盐、地下水除砷、矿泉水除溴的技术汇总

我们所说的“自来水”是指从水龙头里放出来的水。但从水龙头里放出来并不等于安全卫生。实际上,原水必须经过各种处理措施之后才能称为安全卫生的饮用水。每一滴水都要经过了混凝、沉淀、过滤、消毒四个步骤的处理,才能去除杂质和细菌,变得安…

恒运资本:简易程序定增是什么意思?

近年来,在我国股市中,简易程序定增成为了一种受欢迎的融资方法。许多人听过它但并不知道它的含义和工作原理。在本文中,我们将从多个角度来分析简易程序定增。 一、什么是简易程序定增? 简易程序定增是指在不需求经过股东大会批阅…

这6本期刊不再被收录!8月SCISSCI期刊目录已更新~

【SciencePub学术】2023年8月21日,科睿唯安更新了Web of Science核心期刊目录。 此次更新后SCIE期刊目录共包含9496本期刊,SSCI期刊目录共包含3554本期刊。此次SCIE & SSCI期刊目录更新,与上次更新(2023年7月)相比…

AD四层板设计(Altium Designer)

AD绘制四层板 前言一、正片层和负片层介绍二、PCB板的叠层设计1.两层板的叠层2.四层板的叠层 三、规则设计参考资料 前言 1、用 Altium Designer 软件绘制电路时,通常2层板能实现设计需求。遇到板框固定,元器件密集的情况下,2 层板无法实现预…

万字长文带你快速了解整个Flutter开发流程

文章目录 背景1.简介与优势Flutter是什么?为什么选Flutter? 2.开发环境搭建安装Flutter SDK配置开发环境 3.创建项目项目结构概览: 4.UI 构建与布局什么是Widget:StatelessWidget和StatefulWidget:Widget的组合&#x…

不规则透明屏:工作原理和特点应用详解

不规则透明屏是一种新型的显示技术,它可以将图像或视频投射到任意形状的透明屏上,使得观众可以从不同角度观看到清晰的图像。 这种技术可以应用于各种领域,如广告、展览、商场等,具有很大的市场潜力。 不规则透明屏的工作原理是…