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

文章目录

  • [1、Terms 9:Never call virtual functions during construction or destruction](#1、Terms 9:Never call virtual functions during construction or destruction)
    • [1.1为什么不要在构造、析构函数中调用 virtual 函数](#1.1为什么不要在构造、析构函数中调用 virtual 函数)
    • 1.2优化做法:
  • 2、面试相关
  • 3、总结
  • 4、参考

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

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

1.1.1经典错误

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

cpp 复制代码
#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;  
}

编译提示错误信息:

cpp 复制代码
main.cpp: In constructor 'Transaction::Transaction()':
main.cpp:18:19: warning: pure virtual 'virtual void Transaction::logTransaction() const' called from constructor
   18 |     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的实现。

cpp 复制代码
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实现函数。

cpp 复制代码
#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;  
}

输出:

cpp 复制代码
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++》

相关推荐
xzkyd outpaper34 分钟前
Kotlin 协程线程切换机制详解
android·开发语言·kotlin
新手村领路人1 小时前
c++ opencv调用yolo onnx文件
c++·opencv·yolo
啊森要自信1 小时前
【QT】常⽤控件详解(六)多元素控件 QListWidget && Table Widget && Tree Widget
c语言·开发语言·c++·qt
屁股割了还要学1 小时前
【数据结构入门】栈和队列
c语言·开发语言·数据结构·学习·算法·青少年编程
z樾1 小时前
MATLAB核心技巧:从入门到精通
开发语言·matlab
Monkey的自我迭代1 小时前
支持向量机(SVM)算法依赖的数学知识详解
算法·机器学习·支持向量机
暗流者2 小时前
信息安全简要
开发语言·网络·php
阿彬爱学习2 小时前
AI 大模型企业级应用落地挑战与解决方案
人工智能·算法·微信·chatgpt·开源
L.fountain3 小时前
配送算法10 Batching and Matching for Food Delivery in Dynamic Road Networks
算法·配送
laplaya3 小时前
高性能分布式通信框架:eCAL 介绍与应用
c++·分布式