C++不要在构造函数和析构函数中调用虚函数

这里先运行个示例代码:

#include<iostream>  
using namespace std;
class Base {
public:
    Base() {
        cout << "Base::Base()" << endl;
        fun();
        //fun_();//编译错误  
    }
    virtual void fun() {
        cout << "Base::fun()" << endl;
    }
    virtual void fun_() = 0;
    ~Base() {
        cout << "Base::~Base()" << endl;
        fun();
    }
};
class Derived :public Base {
public:
    Derived() {
        cout << "Derived::Derived()" << endl;
        fun();
    }
    virtual void fun() {
        cout << "Derived::fun()" << endl;
    }
    virtual void fun_() {
        cout << "Derived::fun_()" << endl;
    }
    ~Derived() {
        cout << "Derived::~Derived()" << endl;
        fun();
    }
};
int main() {
    Derived* d = new Derived();
    delete d;
    system("pause");
    return 0;
}

输出结果:

Base::Base()    //基类构造
Base::fun()
Derived::Derived()  //子类构造
Derived::fun()
Derived::~Derived()    //子类析构
Derived::fun()
Base::~Base()  //基类析构
Base::fun()

在建立一个对象时,会依次调用父构造函数->子构造函数,同样,在析构时也会逆过来调用子析构函数->父析构函数。那么在调用的时候,如果析构函数中对于虚函数还执行虚机制,就有可能已经执行过一个子对象的析构函数,又去调用子对象的函数,这样会很危险。所以在虚析构函数中,对于虚函数,只会执行目前最外一级的那个函数。

虽然可以对虚函数进行实调用,但程序员编写虚函数的本意应该是实现动态联编。在构造函数中调用虚函数,函数的入口地址是在编译时静态确定的,并未实现虚调用。但是为什么在构造函数中调用虚函数,实际上没有发生动态联编呢?

1. 不要在构造函数中调用虚函数的原因

第一个原因,在概念上,构造函数的工作是为对象进行初始化。在构造函数完成之前,被构造的对象被认为“未完全生成”。当创建某个派生类的对象时,如果在它的基类的构造函数中调用虚函数,那么此时派生类的构造函数并未执行,所调用的函数可能操作还没有被初始化的成员,浙江导致灾难的发生。

第二个原因,即使想在构造函数中实现动态联编,在实现上也会遇到困难。这涉及到对象虚指针(vptr)的建立问题。在Visual C++中,包含虚函数的类对象的虚指针被安排在对象的起始地址处,并且虚函数表(vtable)的地址是由构造函数写入虚指针的。所以,一个类的构造函数在执行时,并不能保证该函数所能访问到的虚指针就是当前被构造对象最后所拥有的虚指针,因为后面派生类的构造函数会对当前被构造对象的虚指针进行重写,因此无法完成动态联编。

2.不要在析构函数中调用虚函数的原因

同样的,在析构函数中调用虚函数,函数的入口地址也是在编译时静态决定的。也就是说,实现的是实调用而非虚调用。
考察如下例子。

#include <iostream>
using namespace std;

class A {
public:
    virtual void show() {
        cout << "in A" << endl;
    }
    virtual ~A() { show(); }
};

class B :public A {
public:
    void show() {
        cout << "in B" << endl;
    }
};

int main() {
    A* a = new A();
    B* b = new B();

    delete a;
    delete b;
    system("pause");
}

程序输出结果是:
in A
in A

在类B的对象b退出作用域时,会先调用类B的析构函数,然后调用类A的析构函数,在析构函数~A()中,调用了虚函数show()。从输出结果来看,类A的析构函数对show()调用并没有发生虚调用。

从概念上说,析构函数是用来销毁一个对象的,在销毁一个对象时,先调用该对象所属类的析构函数,然后再调用其基类的析构函数,所以,在调用基类的析构函数时,派生类对象的“善后”工作已经完成了,这个时候再调用在派生类中定义的函数版本已经没有意义了。

因此,一般情况下,应该避免在构造函数和析构函数中调用虚函数,如果一定要这样做,程序猿必须清楚,这是对虚函数的调用其实是实调用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章