《Effective C++》《构造/析构/赋值运算——9、绝不在构造和析构过程中调用virtual函数》

文章目录

  • 1、Terms 9:Never call virtual functions during construction or destruction
    • 1.1为什么不要在构造、析构函数中调用 virtual 函数
      • 1.1.1经典错误
      • 1.1.2 隐藏错误
    • 1.2优化做法:
  • 2、面试相关
  • 3、总结
  • 4、参考

1、Terms 9:Never call virtual functions during construction or destruction

1.1为什么不要在构造、析构函数中调用 virtual 函数

1.1.1经典错误

假设你有个 class 继承体系,用来塑膜股市交易如买进卖出的订单等等。这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当的记录。

#include <iostream>  // 所有交易的基类  
class Transaction {  
public:  Transaction();  virtual ~Transaction() {} // 虚拟析构函数确保正确释放派生类资源  virtual void logTransaction() const = 0; // 日志记录,因类型不同,自身会有不同的操作  // ... 其他成员函数和成员变量 ...  
};  // Transaction 类的构造函数实现  
Transaction::Transaction() {  // ... 构造函数的实现代码 ...  // 注意:通常不建议在基类的构造函数中调用虚函数,因为这将不会调用派生类的实现  // std::cout << "Transaction constructed" << std::endl; // 示例输出  logTransaction(); // 这将导致编译错误,因为 logTransaction 是纯虚函数  
}  // 派生类 BuyTransaction  
class BuyTransaction : public Transaction {  
public:  BuyTransaction() : Transaction() {} // 确保基类构造函数被调用  virtual void logTransaction() const override {  // 记录此交易类型的日志  std::cout << "BuyTransaction logged" << std::endl;  }  // ... 其他成员函数和成员变量 ...  
};  // 派生类 SellTransaction  
class SellTransaction : public Transaction {  
public:  SellTransaction() : Transaction() {} // 确保基类构造函数被调用  virtual void logTransaction() const override {  // 记录此交易类型的日志  std::cout << "SellTransaction logged" << std::endl;  }  // ... 其他成员函数和成员变量 ...  
};  int main() {  // 创建派生类对象  BuyTransaction buyTx;  SellTransaction sellTx;  // 这里不能直接调用 Transaction 的 logTransaction,因为它是纯虚函数  // 但可以通过派生类对象调用  buyTx.logTransaction();  sellTx.logTransaction();  return 0;  
}

编译提示错误信息:

main.cpp: In constructor ‘Transaction::Transaction():
main.cpp:18:19: warning: pure virtualvirtual void Transaction::logTransaction() const’ called from constructor18 |     logTransaction(); // 这将导致编译错误,因为 logTransaction 是纯虚函数|     ~~~~~~~~~~~~~~^~
/usr/bin/ld: /tmp/ccNYEPdb.o: in function `Transaction::Transaction()':
main.cpp:(.text+0x26): undefined reference to `Transaction::logTransaction() const'
collect2: error: ld returned 1 exit status
报错原因:因为logTransaction函数在Transaction内是一个纯虚函数(pure virtual),
程序无法链接,因为连接器找不到必要的Transaction::logTransaction()的实现代码。

无疑,会有一个 BuyTransaction, SellTransaction构造函数被调用,但是 Transaction 构造函数一定会更早的调用,因为基类会先于派生类构造。
  当执行基类的构造函数时,基类的构造函数调用了虚函数logTransaction()
  由于C++多态的机制,我们实际上想让基类的构造函数调用的是派生类的虚函数logTransaction()(多态:使用一个基类的指针/引用指向于派生类,且派生类重写了基类的虚函数,当用该指针/引用调用虚函数时,调用的是派生类的虚函数)
  但是事实并非如此:当父类的构造函数执行,派生类此时还没有进行构造,因此基类中对logTransaction()的调用不会下降至派生类中,也就是说,此处我们在父类的构造函数中调用的实际上是基类的虚函数logTransaction(),但是由于基类中的logTransaction()函数是纯虚函数,因此程序编译错误。
  在派生类执行基类的构造函数时,派生类此时还未初始化。如果此时在基类的构造函数调用虚函数,调用的实际上是基类的虚函数,对虚函数的调用不会下降到派生类中。
用一句话总结就是:在 base class 构造期间,virtual 函数不再是 virtual 函数。
析构函数
不要在析构函数中调用virtual函数的原理也是相同的:
对象在析构时会先执行自己的析构函数,接着再去执行基类的析构函数
如果在基类的析构函数中调用了虚函数,那么调用的实际上也是基类的虚函数,而不会是派生类的(因为派生类已经在先前被释放了)

1.1.2 隐藏错误

为了避免代码重复的一个优秀做法是把共同的初始化代码(其中包括对logTransaction的调用)放进一个初始化函数init()内:1.1.1中是一个纯虚函数,当pure virtual函数被调用,大多执行系统会中止程序,但是如果是impure virtual函数并在Transaction()函数内部有一份实现代码,那么尽管你是derived的对象,调用的仍然是base class的实现。

class Transaction {
public:Transaction() { init(); }  // 初始化virtual void logTransaction() const = 0; //记录交易日志, 是个虚函数
private:void init() { // 做一些初始化, 比如记录日志等logTransaction(); }
};

1.2优化做法:

解决上述问题的关键,就是将base class内将logTransaction()函数改为non-virtual,然后要求derived class构造函数传递必要的信息给Transaction构造函数,从而更安全地调用non-virtual实现函数。

#include <string>  
#include <iostream>
using namespace std;
class Transaction {  
public:  // 注意:这里添加了分号  explicit Transaction(const std::string& logInfo) { logTransaction(logInfo); }  // 初始化日志信息  void logTransaction(const std::string& logInfo) const {  // 这里是日志记录的逻辑,例如打印到控制台或写入日志文件  // ...  std::cout << "Base Transaction constructed" << std::endl; // 示例输出 std::cout << "Base_construct —— "<< logInfo << std::endl; // 示例输出 }  
};  
class BuyTransaction : public Transaction {  
public:  // 假设BuyTransaction需要商品名称和价格作为参数  BuyTransaction(const std::string& itemName, double price)  : Transaction(createLogString(itemName, price)) {  // 这里可以添加BuyTransaction特有的初始化代码  std::cout << "BuyTransaction logged" << std::endl;  std::cout << "Buying: " << itemName << " at $" << price << std::endl; }  private:  static std::string createLogString(const std::string& itemName, double price) {  // 假设我们创建了一个日志字符串,包含了购买的信息  return "Buy: " + itemName + " at $" + std::to_string(price);  }  
};  
class SellTransaction : public Transaction {  
public:  // 假设SellTransaction需要商品名称和价格作为参数  SellTransaction(const std::string& itemName, double price)  : Transaction(createLogString(itemName, price)) {  // 这里可以添加SellTransaction特有的初始化代码  std::cout << "SellTransaction logged" << std::endl;std::cout << "Selling: " << itemName << " at $" << price << std::endl; }  
private:  static std::string createLogString(const std::string& itemName, double price) {  // 假设我们创建了一个日志字符串,包含了售卖的信息  return "Sell: " + itemName + " at $" + std::to_string(price);  }  
};
int main() {  // 创建一个BuyTransaction对象  BuyTransaction buyTxn("Apple", 1.99);  // 创建一个SellTransaction对象  SellTransaction sellTxn("Orange", 2.49);  // 输出一些信息到控制台以确认对象已经被创建  std::cout << "BuyTransaction and SellTransaction objects have been created." << std::endl;  return 0;  
}

输出:

Base Transaction constructed
Base_construct —— Buy: Apple at $1.990000
BuyTransaction logged
Buying: Apple at $1.99
Base Transaction constructed
Base_construct —— Sell: Orange at $2.490000
SellTransaction logged
Selling: Orange at $2.49
BuyTransaction and SellTransaction objects have been created.

并且此处的createLogString()函数被设置为static函数是比较有意义的,因此静态函数不能调用非静态成员,因此就不会担心createLogString()函数中有未初始化的数据成员

2、面试相关

在构造/析构函数中使用虚函数是一个常见的面试问题,因为这里涉及到一些C++的特性和潜在的问题。以下是关于这个问题的五个高频面试题及其解答:

面试题1:在构造函数中能否调用虚函数?

解答:在构造函数中可以调用虚函数,但此时调用的不是子类覆盖的版本,而是基类自身的版本。这是因为在构造函数执行时,对象的类型还完全是基类的类型,子类部分还没有被构造出来,所以此时调用的虚函数是基类的版本。

面试题2:为什么在构造函数中调用虚函数通常不是一个好主意?

解答:在构造函数中调用虚函数可能导致预期之外的行为,因为此时调用的不是子类覆盖的版本。这可能导致逻辑错误或不符合设计初衷的行为。此外,如果在基类的构造函数中调用虚函数,而该虚函数在子类中又被重写为抛出异常,那么在构造子类对象时可能会抛出异常,这可能导致资源泄露或其他问题。

面试题3:析构函数中能否调用虚函数?

解答:在析构函数中可以调用虚函数,此时调用的是子类覆盖的版本(如果存在的话)。因为在析构函数执行时,对象已经是一个完整的对象,包括基类和子类部分,所以此时调用的虚函数会根据对象的实际类型来确定。

面试题4:析构函数中调用虚函数需要注意什么?

解答:在析构函数中调用虚函数时,需要确保虚函数的实现不会导致任何资源泄露或无效的内存访问。因为析构函数的主要任务是清理资源,如果虚函数的实现不当,可能会破坏这个过程。此外,如果虚函数在子类中被重写为抛出异常,那么在析构函数中调用该虚函数可能会导致程序异常终止,这是需要避免的。

面试题5:如何安全地在析构函数中调用虚函数?

解答:为了安全地在析构函数中调用虚函数,可以采取以下策略:

  1. 确保虚函数的实现是安全的,不会导致资源泄露或无效的内存访问。
  2. 避免在虚函数中抛出异常,特别是在析构函数中。
  3. 如果可能的话,考虑将需要在析构函数中执行的操作封装到另一个非虚函数中,并在基类的析构函数中调用该函数。这样可以确保无论对象的实际类型是什么,都会执行相同的操作。

通过遵循这些原则,可以更安全地在析构函数中调用虚函数,并避免潜在的问题。

3、总结

天堂有路你不走,地狱无门你自来

4、参考

4.1《Effective C++》

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

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

相关文章

网络编程的学习2

UDP通信协议 发送数据 package UDPDEmo;import java.io.IOException; import java.net.*; import java.nio.charset.StandardCharsets;public class SendMessageDemo {public static void main(String[] args) throws IOException {//发送数据//1.创建对象//细节&#xff1a;…

PCL点云库出现错误:..\dist.h(523): error C3861: “pop_t”: 找不到标识符

工程代码&#xff1a;简单地测试了k-d树的最近邻搜索功能 #include<pcl/point_cloud.h> #include<pcl/kdtree/kdtree_flann.h>#include<iostream> #include<vector> #include<ctime>using namespace std;int main(int argc, char** argv) {//使…

回溯算法|78.子集

力扣题目链接 class Solution { private:vector<vector<int>> result;vector<int> path;void backtracking(vector<int>& nums, int startIndex) {result.push_back(path); // 收集子集&#xff0c;要放在终止添加的上面&#xff0c;否则会漏掉自…

Pygame基础8-碰撞

Collisions 在Pygame中&#xff0c;我们使用矩形来移动物体&#xff0c;并且用矩形检测碰撞。 colliderect检测两个矩形是否碰撞&#xff0c;但是没法确定碰撞的方向。 Rect1.colliderect(Rect2) # collision -> return Ture # else -> return Falsecollidepoint可以…

Spring拓展点之SmartLifecycle如何感知容器启动和关闭

Spring为我们提供了拓展点感知容器的启动与关闭&#xff0c;从而使我们可以在容器启动或者关闭之时进行定制的操作。Spring提供了Lifecycle上层接口&#xff0c;这个接口只有两个方法start和stop两个方法&#xff0c;但是这个接口并不是直接提供给开发者做拓展点&#xff0c;而…

如何理解 Java 中的成员变量、字段和属性

在Java编程中&#xff0c;成员变量、字段和属性是关键概念&#xff0c;用于存储对象状态信息的变量。虽然它们经常被用作同义词&#xff0c;但在实际应用中&#xff0c;它们有着微妙的区别。 1. 成员变量&#xff08;Member Variable&#xff09; 成员变量是类中声明的任何变…

AI音乐GPT时刻来临:Suno 快速入门手册!

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

代码块的理解

如果成员变量想要初始化的值不是一个硬编码的常量值&#xff0c;而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值&#xff0c;该怎么办呢&#xff1f;此时&#xff0c;可以考虑代码块&#xff08;或初始化块&#xff09;。 代码块(或初始化块)的作…

前端作业之完成学校官方网页的制作

&#xff08;未使用框架&#xff0c;纯html和css制作&#xff09; 注&#xff1a;由本人技术限制&#xff0c;代码复用性极差 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>xxx大学</tit…

SpringBoot接收参数的方式

Get 请求 1.1 以方法的形参接收参数 1.这种方式一般适用参数比较少的情况 RestController RequestMapping("/user") Slf4j public class UserController {GetMapping("/detail")public Result<User> getUserDetail(String name,String phone) {log.…

zip解压异常java.lang.IllegalArgumentException: MALFORMED处理

使用hutool解压zip包时出错&#xff1a; //压缩包解压到固定目录 ZipUtil.unzip(tempZipFile,dir);在解压文件的时候报错&#xff0c;原因是压缩文件中有中文&#xff1b;导致错误&#xff0c;解决办法是设置编码&#xff1a; ZipFile tempZipFile new ZipFile(zipFile, Cha…

【Linux】详解动静态库的制作和使用动静态库在系统中的配置步骤

一、库的作用 1、提高开发效率&#xff0c;让开发者所有的函数实现不用从零开始。 2、隐藏源代码。 库其实就是所有的.o文件用特定的方式进行打包形成一个文件&#xff0c;各个.o文件包含了源代码中的机器语言指令。 二、动态库和静态库的制作和使用 2.1、静态库的制作和使用…

如何监控特权帐户,保护敏感数据

IT基础设施的增长导致员工可以访问的凭据和资源数量急剧增加。每个组织都存储关键信息&#xff0c;这些信息构成了做出关键业务决策的基石。与特权用户共享这些数据可以授予他们访问普通员工没有的凭据的权限。如果特权帐户凭证落入不法分子之手&#xff0c;它们可能被滥用&…

使用WebRTC实现简单直播

WebRTC 是一个强大的实时通信技术&#xff0c;它允许用户直接在网页浏览器之间进行音视频通话和数据共享&#xff0c;无需任何外部插件。结合 WebSocket&#xff0c;我们可以构建一个简单的直播系统&#xff0c;让用户能够发布自己的实时视频流&#xff0c;同时允许其他用户观看…

请描述一下Velocity模板中的循环结构是如何工作的。Velocity有哪些内置的函数和方法?能否举例说明它们的使用场景?

请描述一下Velocity模板中的循环结构是如何工作的。 Velocity是一个基于Java的模板引擎&#xff0c;它允许开发人员使用简单的模板语言来引用由Java代码定义的对象&#xff0c;并在生成的文本中呈现这些对象。在Velocity模板中&#xff0c;循环结构用于遍历集合或数组&#xff…

【重学C语言】三、C语言最简单的程序

【重学C语言】三、C语言最简单的程序 最简单的程序头文件使用尖括号 < >使用双引号 ""区别与注意事项示例 主函数认识三个错误 常量和变量常量ASCII 码表转义字符 关键字数据类型关键字存储类关键字修饰符关键字控制流程关键字函数相关关键字其他关键字 变量变…

Python中批量修改文件名,去除某些内容

环境&#xff1a;Window10 Python3.9 PyCharm(2023.1.3) -------------------------------------****************** ** *********************----------------------------------------- 这是在Python中批量将指定文件夹下相似的文件名&#xff0c;提取文件名有效信息&am…

llama.cpp运行qwen0.5B

编译llama.cp 参考 下载模型 05b模型下载 转化模型 创建虚拟环境 conda create --prefixD:\miniconda3\envs\llamacpp python3.10 conda activate D:\miniconda3\envs\llamacpp安装所需要的包 cd G:\Cpp\llama.cpp-master pip install -r requirements.txt python conver…

什么!Intel/AMD/Apple Silicon也能本地部署的Llama工具来了

主流的LLM都需要通过CUDA才能高效的运行在本地&#xff0c;但是随着Github上出现了Llama.cpp这个神器&#xff0c;一切都改变了。它通过AVX指令和MPI来实现CPU上并行计算&#xff0c;从而在本地计算机高效地运行各种主流的类Llama模型。同时它也支持metal&#xff0c;使得Apple…

保险项目的模块

用户管理模块 注册登录用户信息管理&#xff08;个人资料、密码重置等&#xff09;权限管理 保单管理模块 保单申请保单查询保单修改保单终止 理赔管理模块 理赔申请理赔审核理赔支付 保险产品管理模块 产品信息管理产品定价产品推广 支付与结算模块 支付方式管理保费…