今天为了查一个重复delete的bug,在析构函数中调用了一个虚函数 toString,想在对象析够前打印对象信息,结果发现打印出来全都是基类的,后来仔细研究了这个问题,先说结论:
1,绝对不要在构造函数和析构函数中调用虚函数,他们都不是动态绑定的。
2,如果析构函数是虚函数,那么可以看到类似动态绑定的效果,但这并不是动态绑定,也并不意味着我们可以随意在析构函数中调用、间接调用虚函数
3,如果基类存在虚函数,析构函数必须为虚函数,更进一步的讲,如果程序可能会存在 base *p = new drived() 这样的语句,即使没有虚函数也要有虚析构函数
4, 为什么会产生这样的效果??
因为vptr的绑定是发生在对象构造期间的,所以构造函数会有这样一个假设: 我正在构造的对象和我的类型是完全一致的, 同理虚析构函数也会这么假设。所以在构造/析构函数中调用virtual 函数时不会有任何多态效果的。
5, 那为什么虚析构函数中调用虚函数会有多态??
因为析构函数本身如果是虚函数的,在执行虚函数之前会通过vptr找到正确的析构函数,然而在那个析构函数调用的虚函数依旧是没有动态绑定。我们看到的"动态绑定"是虚析构函数的动态绑定,而不是析构函数所调用的虚函数的多态
6,考虑下面的代码:
class Base { public: virtual void wtf() { cout << "base" << endl; } virtual ~Base() { wtf(); } }; class Drived : public Base { public: void wtf() { cout << "drived" << endl; } ~Drived () { wtf(); } }; int main() { Base *p = new Drived(); delete p; return 0; }
输出为 drived base, 看起来似乎一切正常,但是实际上的运行过程是:由于析构函数动态绑定,所以在delete p时,发现对象实际类型为drived,所以先析构派生类,再析构基类, 所以执行drived::~drived(), 在drived::~drived() 中看见了一个叫wtf()的函数,这个时候不会去查vtab,直接认为它是drived::wtf(),之后执行base::~base()同理。
但是如果析构函数不是虚函数,那么就只会执行base::~base() 和 base::wtf(),哪怕 wtf是虚函数。更进一步的,凡是代码中发生类似base *p = new dirved()情况的,几乎都必须要有一个虚析构函数。否则就会出现delete p时只会delete基类部分。。。
更更更扩展的是,使用 delete [] p 时,为了判断delete的次数和位置, 即使有虚析构函数还是会出问题,因为具体在哪些位置delete 是未定义的。。
同样的,在函数构造是,会先执行基类构造函数,和基类对应版本的函数,然后是子类的。
所以不要尝试在 构造/析构 函数中调用虚函数, 这会让程序的意义非常不明确。如果非得调用,显式说明我要调用那个版本的,如base::xx()