• Effective C++条款09:绝不在构造和析构过程中调用virtual函数


    Effective C++条款09:绝不在构造和析构过程中调用virtual函数(Never call virtual functions during construction or destruction)


    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


    条款09:绝不在构造和析构过程中调用 virtual 函数

      本条款要阐述的重点:你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。

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

    构造函数

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

    class Transaction { // 所有交易的基类
    public:
        Transaction() { logTransaction(); } //记录日志
     
        virtual void logTransaction() const = 0; //记录交易日志, 是个虚函数
        ...
    };
     
    class BuyTransaction : public Transaction //购买交易,派生类
    {
    public:
        virtual void logTransaction() const;
    };
     
    // 销售交易
    class SellTransaction : public Transaction 
    {
    public:
        virtual void logTransaction() const;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    现在执行以下代码:

    BuyTransaction  b;
    
    • 1

      无疑,会有一个 BuyTransaction 构造函数被调用,但是 Transaction 构造函数一定会更早的调用,因为基类会先于派生类构造。

      当执行基类的构造函数时,基类的构造函数调用了虚函数logTransaction()

      由于C++多态的机制,我们实际上想让基类的构造函数调用的是派生类的虚函数logTransaction()(多态:使用一个基类的指针/引用指向于派生类,且派生类重写了基类的虚函数,当用该指针/引用调用虚函数时,调用的是派生类的虚函数)

      但是事实并非如此:当父类的构造函数执行,派生类此时还没有进行构造,因此基类中对logTransaction()的调用不会下降至派生类中,也就是说,此处我们在父类的构造函数中调用的实际上是基类的虚函数logTransaction(),但是由于基类中的logTransaction()函数是纯虚函数,因此程序编译错误。

    在派生类执行基类的构造函数时,派生类此时还未初始化。如果此时在基类的构造函数调用虚函数,调用的实际上是基类的虚函数,对虚函数的调用不会下降到派生类中。

    用一句话总结就是:在 base class 构造期间,virtual 函数不再是 virtual 函数。

    析构函数

    不要在析构函数中调用virtual函数的原理也是相同的:

    • 对象在析构时会先执行自己的析构函数,接着再去执行基类的析构函数

    • 如果在基类的析构函数中调用了虚函数,那么调用的实际上也是基类的虚函数,而不会是派生类的(因为派生类已经在先前被释放了)

    优化做法

    如果 Transaction 有多个构造函数,每个都需要执行某些相同工作,那么避免代码重复的一个优秀做法有把共同的初始化代码(其中包括对 logTransaction的调用)放进一个初始化函数如 init 内:

    class Transaction {
    public:
        Transaction() { init(); }  // 初始化
     
        virtual void logTransaction() const = 0; //记录交易日志, 是个虚函数
    private:
        void init() { 
            // 做一些初始化, 比如记录日志等
            logTransaction(); 
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      上面在构造函数调用一个初始化函数init(),在init()中调用logTransaction()虚函数,但是也是错误的,因为logTransaction()虚函数调用的仍是基类版本的,此时编译仍报错。

      而真正正确的方法则是:确定你的构造函数和析构函数都没有(在对象创建和被销毁期间)调用 virtual 函数而它们调用的所有函数都服从同一约束。

    具体做法:

    • 在 Transaction 类中将 logTransaction() 函数改为 non-virtual 函数。然后要求子类构造函数传递必要信息给 Transaction 构造函数,而后那个构造函数便可安全地调用 non-virtual logTransaction()。如:
    class Transaction {
    public:
        explicit Transaction(const std::string& logInfo) { logTransaction(logInfo) }
     
        // 初始化日志信息
        void logTransaction(const std::string& logInfo) const;
    };
     
     
    class BuyTransaction : public Transaction {
    public:
        // 构造自己时同时初始化基类
        BuyTransaction(相关参数): Transaction(createLogString(相关参数)) {}
    private:
        static std::string createLogString(相关参数);
    };
     
    class SellTransaction : public Transaction {
    public:
        // 构造自己时同时初始化基类
        SellTransaction(相关参数): Transaction(createLogString(相关参数)) {}
    private:
        static std::string createLogString(相关参数);
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 并且此处的createLogString()函数被设置为static函数是比较有意义的,因此静态函数不能调用非静态成员,因此就不会担心createLogString()函数中有未初始化的数据成员

    小结

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

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    数组扁平化实现
    Redis-布隆过滤器(Bloom Filter)
    WindowsNT下的OpenGL
    【WPF】CAD工程图纸转WPF可直接使用的xaml代码技巧
    MySQL 增删改 insert delete update 实例
    Java-lambda表达式
    PLC信号发生器(余弦信号)
    C++ Tutorials: C++ Language: Other language features: Exceptions
    AJAX——HTTP协议
    UE4光照基础
  • 原文地址:https://blog.csdn.net/CltCj/article/details/127963923