[翻译] Effective C++, 3rd Edition, Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)

LDO稳定性分析 根据补偿方式和拓扑结构的不同,三个极点之间的相互位置有可能发生变化,但 是 LDO 通常采用较大的输出电容 Cout,以及较大的调整管也会导致较大的 Cpar, 所以 p1、p2 通常位于较低的频率处,而 p3 和 z1 则由于 Resr较小会位于较高的频 率处。不过幸运的是,对于较大的 Cout来说,只要选择合适的 Resr,就可以将左半平面零点 Z1 的频率降低,甚至使 Z1 进入单位增益带宽内,左半平面零点的进入将对相位有所改善,这不仅可以消除振荡的风险,还可以在单位增益频率处得到较好的相位裕度。 阅读详情

Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.youkuaiyun.com/fatalerror99/

我以这个概述开始:你不应该在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数),因为这样的调用不会如你想象那样工作,而且它们做的事情保证会让你很郁闷。如果你转为 Java 或 C# 程序员,也请你密切关注本 Item,因为在 C++ 急转弯的地方,那些语言也紧急转了一个弯。

假设你有一套模拟股票交易的 class hierarchy(类继承体系),例如,购入订单,出售订单等。对于这样的交易来说可供审查是非常重要的,所以每次一个交易对象被创建,在一个审查日志中就需要创建一个相应的条目。下面是一个看起来似乎合理的解决问题的方法:

class Transaction {                               // base class for all
public:                                           // transactions
  Transaction();

  virtual void logTransaction() const = 0;        // make type-dependent
                                                  // log entry
  ...
};

Transaction::Transaction()                        // implementation of
{                                                 // base class ctor
  ...
  logTransaction();                               // as final action, log this
}                                                 // transaction

class BuyTransaction: public Transaction {        // derived class
public:
  virtual void logTransaction() const;            // how to log trans-
                                                  // actions of this type
  ...
};

class SellTransaction: public Transaction {       // derived class
public:
 virtual void logTransaction() const;             // how to log trans-
                                                  // actions of this type
  ...
};

考虑执行这行代码时会发生什么:

BuyTransaction b;

很明显一个 BuyTransaction 的 constructor(构造函数)会被调用,但是首先,一个 Transaction 的 constructor(构造函数)必须先被调用,derived class objects(派生类对象)中的 base class parts(基类构件)先于 derived class parts(派生类构件)被构造。Transaction 的 constructor(构造函数)的最后一行调用 virtual functions(虚拟函数) logTransaction,但是结果会让你大吃一惊,被调用的 logTransaction 版本是在 Transaction 中的那一个,而不是 BuyTransaction 中的那一个——即使被创建的 object (对象)类型是 BuyTransaction。base class construction(基类构造)期间,virtual functions(虚拟函数)从来不会 go down(向下匹配)到 derived classes(派生类)。取而代之的是,那个 object (对象)的行为好像它就是 base type(基类型)。非正式地讲,base class construction(基类构造)期间,virtual functions(虚拟函数)被禁止。

这个表面上看起来匪夷所思的行为存在一个很好的理由。因为 base class constructors(基类构造函数)在 derived class constructors(派生类构造函数)之前执行,当 base class constructors(基类构造函数)运行时,derived class data members(派生类数据成员)还没有被初始化。如果 base class construction(基类构造)期间 virtual functions(虚拟函数)的调用 went down(向下匹配)到 derived classes(派生类),derived classes(派生类)的函数差不多总会涉及到 local data members(局部数据成员),但是那些 data members(数据成员)至此还没有被初始化。这就会为 undefined behavior(未定义行为)和通宵达旦的调试噩梦开了一张通行证。调用涉及到一个 object(对象)还没有被初始化的构件自然是危险的,所以 C++ 告诉你此路不通。

实际上还有比这更基本的原理。在一个 derived class object(派生类对象)的 base class construction(基类构造)期间,object(对象)的类型是 base class(基类)的类型。不仅 virtual functions(虚拟函数)会解析到 base class(基类),而且用到 runtime type information(运行时类型信息)的语言构件(例如,dynamic_cast(参见 Item 27)和 typeid),也会将那个 object(对象)视为 base class type(基类类型)。在我们的例子中,当 Transaction 的 constructor(构造函数)运行到初始化一个 BuyTransaction object(对象)的 base class(基类)部分时,那个 object(对象)的是 Transaction 类型。C++ 的每一个构件将以如下眼光来看待它,而且这种看法是合理的:这个 object(对象)的 BuyTransaction-specific 的构件还没有被初始化,所以对它们视若无睹是最安全的。直到 derived class constructor(派生类构造函数)的执行开始之前,一个 object(对象)不会成为一个 derived class object(派生类对象)。

同样的推理也适用于 destruction(析构)。一旦 derived class destructor(派生类析构函数)运行,这个 object(对象)的 derived class data members(派生类数据成员)就呈现为未定义的值,所以 C++ 就将它们视为不再存在。在进入 base class destructor(基类析构函数)时,这个 object(对象)就成为一个 base class object(基类对象),C++ 的所有构件—— virtual functions(虚拟函数),dynamic_casts 等——都以此看待它。

在上面的示例代码中,Transaction 的 constructor(构造函数)造成了对一个 virtual functions(虚拟函数)的一次直接调用,是对本 Item 的指导建议的显而易见的违背。这一违背是如此显见,以致一些编译器会给出一个关于它的警告。(另一些则不会。参见 Item 53 对于警告的讨论。)即使没有这样的一个警告,这个问题也几乎肯定会在运行之前暴露出来,因为 logTransaction 函数在 Transaction 中是 pure virtual(纯虚拟)的。除非它被定义(不太可能,但确实可能——参见 Item 34),否则程序将无法连接:连接程序无法找到 Transaction::logTransaction 的必要的实现。

在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)的问题并不总是如此容易被察觉。如果 Transaction 有多个 constructors(构造函数),每一个都必须完成一些相同的工作,软件工程为避免代码重复,将共通的 initialization(初始化)代码,包括对 logTransaction 的调用,放入一个 private non-virtual initialization function(私有非虚拟初始化函数)中,叫做 init:

class Transaction {
public:
  Transaction()
  { init(); }                                     // call to non-virtual...

  virtual void logTransaction() const = 0;
  ...

private:
  void init()
  {
    ...
    logTransaction();                             // ...that calls a virtual!
  }
};

这个代码在概念上和早先那个版本相同,但是它更阴险,因为一般来说它会躲过编译器和连接程序的抱怨。在这种情况下,因为 logTransaction 在 Transaction 中是 pure virtual(纯虚的),在 pure virtual(纯虚)被调用时,大多数 runtime systems(运行时系统)会异常中止那个程序(一般会对此结果给出一条消息)。然而,如果 logTransaction 在 Transaction 中是一个 "normal" virtual function(“常规”虚拟函数)(也就是说,not pure virtual(非纯虚拟的)),而且带有一个实现,那个版本将被调用,程序会继续一路小跑,让你想象不出为什么在 derived class object(派生类对象)被创建的时候会调用 logTransaction 的错误版本。避免这个问题的唯一办法就是确保你的 constructors(构造函数)或 destructors(析构函数)决不在被创建或销毁的 object(对象)上调用 virtual functions(虚拟函数),它们所调用的全部函数也要服从同样的约束。

但是,你如何确保在每一次 Transaction hierarchy(继承体系)中的一个 object(对象)被创建时,都会调用 logTransaction 的正确版本呢?显然,在 Transaction constructor(s)(构造函数)中在这个 object(对象)上调用 virtual functions(虚拟函数)的做法是错误的。

有不同的方法来解决这个问题。其中之一是将 Transaction 中的 logTransaction 转变为一个 non-virtual function(非虚拟函数),这就需要 derived class constructors(派生类构造函数)将必要的日志信息传递给 Transaction constructor(构造函数)。那个函数就可以安全地调用 non-virtual(非虚拟)的 logTransaction。如下:

class Transaction {
public:
  explicit Transaction(const std::string& logInfo);

  void logTransaction(const std::string& logInfo) const;   // now a non-
                                                           // virtual func
  ...
};

Transaction::Transaction(const std::string& logInfo)
{
  ...
  logTransaction(logInfo);                                 // now a non-
}                                                          // virtual call

class BuyTransaction: public Transaction {
public:
 BuyTransaction( parameters )
 : Transaction(createLogString( parameters ))              // pass log info
  { ... }                                                  // to base class
   ...                                                     // constructor

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

换句话说,由于你不能在 base classes(基类)的 construction(构造)过程中使用 virtual functions(虚拟函数)向下匹配,你可以改为让 derived classes(派生类)将必要的构造信息上传给 base class constructors(基类构造函数)作为补偿。

在此例中,注意 BuyTransaction 中那个 (private) static 函数 createLogString 的使用。使用一个辅助函数创建一个值传递给 base class constructors(基类构造函数),通常比通过在 member initialization list(成员初始化列表)给 base class(基类)它所需要的东西更加便利(也更加具有可读性)。将那个函数做成 static,就不会有偶然触及到一个新生的 BuyTransaction object(对象)的 as-yet-uninitialized data members(仍未初始化的数据成员)的危险。这很重要,因为实际上那些 data members(数据成员)处在一个未定义状态,这就是为什么在 base class(基类)construction(构造)和 destruction(析构)期间调用 virtual functions(虚拟函数)不能首先向下匹配到 derived classes(派生类)的原因。

Things to Remember

  • 在 construction(构造)或 destruction(析构)期间不要调用 virtual functions(虚拟函数),因为这样的调用不会转到比当前执行的 constructor(构造函数)或 destructor(析构函数)所属的 class(类)更深层的 derived class(派生类)。
消费级显卡跑大模型:RTX 3060 12G 部署 70 亿参数 AI 编程助手的优化实战 本文详细介绍了如何在消费级显卡RTX 3060 12G上,通过量化、投机解码和分页KV-Cache等核心优化技术,成功部署并流畅运行70亿参数的CodeLlama-7B编程助手。文章提供了从环境配置、模型准备到参数调优的完整实战笔记,实测显存占用降至7.3GB,生成速度达21.7 token/秒,为开发者和技术爱好者提供了可行的本地AI部署方案。 阅读详情

相关推荐

STLink驱动安装全攻略:从卸载到Keil配置的完整避坑指南

本文提供了STLink驱动安装与Keil环境配置的完整避坑指南。详细讲解了从驱动卸载、重装到Keil调试器设置的每一步,并针对常见的设备无法识别、连接不稳定等问题给出了深度解决方案,帮助开发者快速搭建稳定的STM32开发环境。

weixin_29182481的博客 426

[C++] 关于 构造函数(construction) 析构函数(destruction)中调用虚函数

在“构造函数”/“析构函数”中,调用虚函数需要通过类命调用“成员函数” 通常和多继承下,对有歧义的函数,需要“类名::函数名”的方式调用,如,定义与“基类的函数”同名的函数(特别是'重写') class Animal { public:  Animal() { ... };  virtual void Run() { ... }  virtual void Walk(){ ... }

AS588AS的博客 1346

UVC协议在STM32H7上的移植实战案例

手把手带你把UVC协议移植到STM32H7平台,重点解决USB高速枚举、视频流控制与YUV数据打包等关键问题。基于HAL库实现符合UVC 1.5规范的即插即用摄像头功能,让嵌入式设备轻松变身标准UVC视频源。

weixin_42437253的博客 397

深挖C++:类和对象(中)

(先进后出)

xys_678的博客 254

将AAL图谱对应到Yeo7大网络中【原理,代码分析】

本文介绍了将AAL2图谱映射到Yeo7大功能网络的方法。通过整合皮层(Yeo2011)、小脑(Buckner2011)和纹状体(Choi2012)三个区域的功能网络图谱,采用投票机制确定每个AAL区域的网络归属。具体步骤包括:1)统计每个AAL区域体素在各网络图谱中的分布;2)选择体素数量最多的网络作为该区域的最终标签。该方法实现了从解剖结构到功能网络的映射,可获得120个AAL区域对应的7大功能网络标签。

yes666666的博客 2207

Cadence元件库介绍

Cadence安装完之后,在安装目录下,会有默认自带的一些常用元件库。例如,我的Candece安装目录为: D:\Cadence\Cadence_SPB_17.2-2016 元件库所在目录为: D:\Cadence\Cadence_SPB_17.2-2016\tools\capture\library 各个元件库对应的内容如下: AMPLIFIER.OLB 共 182 个零件,存放模拟放大器 IC,如 CA3280,TL027C,EL4093 等。 ARITHMETIC.OLB 共 182 个零件,存放

年少时的棉花糖 1万+

C++移动构造函数详解

移动构造函数是C++11引入的一种特殊构造函数,用于从临时对象(右值)"窃取"资源来初始化新对象,而不是进行昂贵的深拷贝。char* data;public:// 移动构造函数:参数是右值引用 &&// 关键:把源对象的资源置空。

拒绝AI文,努力在AI时代写出感动人心的好文章 394

3.嵌入式控制器EC学习,源码框架(60/64端口)(一)

服务函数剖析 上一篇讲到while循环会一直查询十六个标志位是否满足要求,一旦其中某个标志位为1时,程序将会执行相应的服务函数,而执行服务函数是通过函数指针(service_table[ _R1 ])();来实现的。接下来我们分别来看看这十六个服务函数分别包含了哪些内容。 16个函数的函数名如下: const FUNCT_PTR_V_V code service_table[16] = { // Hi-Level Service Service_PCI, // Host 6

qq_30135687的博客 4160

ThunderKittens 深度解析:一个不基于 MLIR 的 C++ 模板 GPU DSL

Triton 是"让更多人能写 GPU kernel",ThunderKittens 是"让会写的人写得更爽"。前者用 MLIR 换易用性,后者用 C++ 模板换可控性。它们不是替代关系,而是覆盖了 GPU 编程光谱的两端。ThunderKittens 的出现,本质上是对"GPU DSL 必须基于 MLIR"这一默认假设的挑战。编译期类型系统本身就可以是一个强大的 DSL 基础设施,不需要中间 IR,不需要黑盒 pass,也能实现 tile 级抽象和零运行时开销。

Maxwell_Newton的博客 349

三菱FX5U气缸控制功能块(完整ST源代码+示例程序)

如果机器包含100个气缸,我们只需要修改数组的元素数量就可以了,效率非常的高。待续....博途PLC 面向对象系列之“双通气缸功能块“(SCL代码)_plc面向对象-优快云博客文章浏览阅读667次。本文介绍了博途PLC中面向对象编程的应用,重点讲解了双通气缸的功能块设计,包括双通电磁阀功能块、双通气缸的UDT数据类型以及SCL代码实现。同时,对比了双通与单通电磁阀的差异,并讨论了将单通气缸改造为双通气缸的方法,强调了双通气缸在安全性上的优势。

RXXW_Dor的博客 1029

816华为OD机试真题 新系统 - 电池充电时间计算 (Java/Py/C/C++/Js/Go)

816华为OD机试真题 新系统 - 电池充电时间计算 (Java/Py/C/C++/Js/Go)

算法大师! 475

跟我学c++中级篇—类型定义的方式

本文系统阐述了编程中类型定义的重要性及c++中的实现方式。重点对比了宏定义、typedef与modern using三种机制,指出using在可读性、灵活性和模板支持上的优势,推荐优先使用。同时提醒开发者注意别名滥用、类型歧义、错误信息冗长等问题,强调统一规范与合理设计。最终结论:在现代c++开发中,应以using为主,兼顾简洁性与可维护性,避免陷入命名混乱与编译困境。

fpcc的专栏 88

微信商家转账报错“当前使用用户过多,请稍后重新确认收款”解决方案

当前使用用户过多,请稍后重新确认收款”这个错误,其实就是典型的参数误处理导致的假报错。记住一句话就行👇JSAPI 场景下的package参数不要做 UrlEncode。如果你也遇到这个问题,希望这篇文章能帮你少踩一个坑。

代码简单说 Vue、JAVA、PHP、Node.js 熟练运用,接口、架构、性能全搞定。 785

【C++——类和对象(上)】筑基篇:以封装奠定面向对象之基

本文系统梳理C++类与对象的核心知识点,涵盖类的定义、访问限定符(public/private)、类作用域、对象实例化及内存布局。重点解析封装思想、this指针机制、空类大小为1字节的原因,以及类与对象在内存中的存储差异。通过对比C语言结构体与C++类,揭示面向对象设计的优势:数据保护、接口统一、命名隔离与代码可维护性提升。内容由浅入深,适合作为学习笔记与复习参考。

2602_95684935的博客 498

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

C++ 函数模版

模板参数:● template ,T 就是类型占位符,名字可以自定义,习惯用T● 占位符可以在函数返回值、形参、函数内部当作类型使用使用要点模板代码一般写在头文件:编译器需要看到完整模板源码,才能在每个编译单元完成实例化全局函数、类内成员函数都可以写成模板实例化发生在编译阶段,不是运行时隐式实例化:调用时编译器自动推导实参类型,生成对应版本函数。

2501_94041724的博客 307

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

C++ 运行时多态与虚函数机制全面剖析【面试重点】

构成C++ 运行时多态必须是基类的指针或者引用调用虚函数。被调用的函数必须是虚函数,并且派生类必须对该虚函数完成重写/覆盖(Override)。只有基类的指针或引用才能既指向基类对象,又指向派生类对象(切片机制);而只有完成了虚函数重写,派生类与基类才具备不同的函数行为,达到“指向谁就调用谁”的效果。在类成员函数的前面加上virtual关键字修饰,该成员函数就成为了虚函数。public:// 成员函数前加 virtual 修饰,声明为虚函数。

Fuyo_0209的博客 450

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

#SQLite 数据库笔记一:(概念 / 终端指令 / SQL 语句 / C 编程 / 回调函数)

数据库 = 数据的仓库,假设数据是海量。能力说明存储数据持久化保存,程序退出数据还在统计数据、分组、排序数据管理增、删、改、查(CRUD)嵌入式设备上也会用数据库(单片机网关、路由器、工业 HMI 都常见),因为 sqlite 体积小、无需服务进程、单文件即可。数据库 = 库 > 表 > 记录(行) > 字段(列)sqlite3 = 开源、C 写的、单文件、免安装、最多 2T 的嵌入式关系型数据库。

lwq20051110的博客 251

1-10 PostgreSQL中多表连接全解:inner/left/right/full/cross join

本文基于 PostgreSQL 15+ 完整拆解 5 种主流连接:内连接、左连接、右连接、全连接、笛卡尔连接。结合可落地的测试数据,演示每种连接的执行逻辑、返回结果,重点剖析 NULL 值对 JOIN 匹配的核心影响,彻底解决多表连接数据丢失、匹配异常、结果错乱的问题。

zhangjw34的专栏 105

【C++ 基础入门】C++继承(下):问题、底层原理与组合

C++继承(菱形继承的问题、虚继承原理、继承与组合、构造顺序)

2601_96587588的博客 802

蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级 备考:莫队算法离线区间查询详解

备赛进行时:第十八届蓝桥杯软件赛报名已于 2026-10-08 10:00 开启、12-18 14:00 截止,省赛定在 2027-04-10(C/C++·Python 程序设计 9:00–13:00);中国电子学会 2026 年 12 月软件编程(C/C++ 一级至十级、Python 一级至六级)考级报名 9/23–12/2 16:00、统考 12-12;全国青少年信息素养大赛 2026—2027 赛季算法应用主题赛也已进入复盘练兵窗口。

IT_Scratch的博客 170

c++ explicit关键字详解

explicit。

拒绝AI文,努力在AI时代写出感动人心的好文章 151

825 mW 顶上 212 °C:ASM491S 的 105 °C/W、250 mA 短路电流与热关断

25 °C 环境里结温 34.1 °C,125 °C 环境里结温 134.1 °C——后者已经离 150 °C 只有 16 °C 了,但还在范围内。换个角度看同一件事:从 25 °C 和从 125 °C 起算,温升都是 86.6 °C,但一个到 111.6 °C(还在范围内),一个到 211.6 °C(远远超限)。这个算例里 20 °C/W 是我假设的,实际值取决于机壳材料、密封程度、有没有风扇,跨度可以从 5 °C/W 到 50 °C/W 以上。外面 50 °C 的机箱,盒内就是 63.9 °C。

ANSILIC的博客 393

《C++ 项目:手写大模型接入 SDK》Gemini接口设计(上):初始化、全量响应实现和测试

本文系统完成了Gemini模型通过OpenAI兼容API的接入开发,基于Apifox验证接口规范,设计并实现GeminiProvider类,涵盖初始化、状态检测、全量响应等核心功能。重点处理了代理配置强制校验、Bearer鉴权、JSON序列化与响应解析等工程细节,并通过GTest与CMake完成单元测试,验证链路可用性。项目遵循策略模式,保持与DeepSeek、ChatGPT等模型的统一抽象接口,显著提升SDK可扩展性。

2501_91275995的博客 398

【容斥原理计数】题解:P9489 ZHY的表示法_容斥原理_计数问题_前缀和问题转化_算法竞赛C++

给定 $ n $ 个正整数,求区间 $[l,r]$ 中有多少正整数 $ x $ 可表示为 $ \sum \lfloor \frac{y}{x_i} \rfloor $。通过二分确定最大 $ y $,转化为求 $ 1 \sim H $ 中是某些 $ x_i $ 倍数的数的个数,利用容斥原理与 DFS 枚举子集计算,结合 LCM 预判剪枝及去重优化,时间复杂度 $ O(2^n \log V) $,可高效处理 $ r \le 10^9 $ 的数据。

YLCHUP的博客 106

头歌 数据结构——C&C++线性表实训

本系列任务围绕线性表的链式实现,依次完成顺序构建、逆序构建、排序插入、查找、删除、求长度及栈、队列、集合等应用。通过动态内存分配与指针操作,掌握单链表的基本操作与实际应用,涵盖插入、删除、遍历、搜索等核心功能,最终实现数据结构在典型场景中的灵活运用。

一名敲代码的苦13程序员 244

字母移动游戏题解:贪心求最长可保留连续子串(附 C 实现)

本题核心思想是:最少移动步数 = 总长度 - 可保留的最长连续子串长度。未移动字母在T中必须为连续子串,在S中需保持相对顺序(即子序列)。通过枚举T的所有连续子串起点,贪心匹配S中的对应字符,求出最长可保留长度L,答案即为n−L。时间复杂度O(n²),空间复杂度O(n),适用于n≤1000。

冻柠茶飞冰走茶 592
上一篇: [翻译] Effective C++, 3rd Edition, Item 8: 防止因为 exceptions(异常)而离开 destructors(析构函数)
下一篇: [翻译] Effective C++, 3rd Edition, Item 10: 让 assignment operators(赋值运算符)返回一个 reference to *this(引向 *this 的引用)
fatalerror99
博客等级 码龄24年 166粉丝 20原创
评论 6
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值