定义

顾名思义,虚析构函数就是给析构函数声明为 virtual。

作用

虚析构函数可以正常的销毁多态模式下的派生类对象,防止造成一个诡异的“局部销毁”对象,从而防止形成内存泄漏。

使用场景

  • 带有多态性质的基类应该声明一个 virtual 析构函数。如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数。
  • 类的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明为 virtual 析构函数。

虚析构函数

OK,上文已经总结了关于 virtual 析构函数的有关内容,接下来咱们仔细分析virtual 析构函数的用途以及到底为啥要声明 virtual 析构函数。

先来看一个示例:

#include <iostream>

using namespace std;

class Shape
{
public:
Shape() {}
~Shape(){}
virtual void draw() = 0;
};

class Rectange : public Shape
{
public:
Rectange() {}
~Rectange(){}
void draw(){
//do something
cout << "draw rectange.." << endl;
}
};
class Circle : public Shape
{
public:
Circle() {}
~Circle(){}
void draw(){
//do something
cout << "draw Circle.." << endl;
}
};
class Triangle : public Shape
{
public:
Triangle() {}
~Triangle(){}
void draw(){
//do something
cout << "draw Triangle.." << endl;
}
};

int main()
{
Shape * pRectange = new Rectange();
Shape * pCircle = new Circle();
Shape * pTriangle = new Triangle();

pRectange->draw();
pCircle->draw();
pTriangle->draw();

delete pRectange;
delete pCircle;
delete pTriangle;

return 0;
}

首先,我们定义了一个图形基类Shape,并声明一个绘图接口 draw(),留给派生类去实现,然后分别有三个派生类继承于 Shape,然后在初始化对象的时候,动态创建派生类对象并赋给基类指针,通过基类指针对象调用绘图接口 draw()可以自动调用每个子类中的实现。这是一个最简单的多态结构。

那么问题就来了,动作执行完过后,我们通过 delete 删除基类指针,由于基类中的析构函数并不是虚析构函数,这就会引来一个灾难性问题。

C++明确指出,当派生类对象经由一个基类指针被删除,而该基类带着一个non-virtual析构函数(非虚析构函数),其结果未有定义-----实际执行时通常发生的是对象的派生成分没被销毁。

也就是说,当执行delete pRectange;时,Rectange内的成员变量很有可能没有被销毁,并且Rectange的析构函数也没有执行。
我们将上述代码中添加析构函数中的打印,如下:

~Rectange(){
cout << "this is ~Rectange()" << endl;
}

执行程序发现,执行delete pRectange;时并没有打印该析构函数中的输出。

然而基类成分通常会被销毁,于是造成一个诡异的“局部销毁对象”。这将导致资源泄露。

正确的做法就是将基类析构函数声明为 virtual,如下:

#include <iostream>

using namespace std;

class Shape
{
public:
Shape() {}
virtual ~Shape(){
cout << "this is ~Shape()" << endl;
}
virtual void draw() = 0;
};

class Rectange : public Shape
{
public:
Rectange() {}
~Rectange(){
cout << "this is ~Rectange()" << endl;
}
void draw(){
//do something
cout << "draw rectange.." << endl;
}
};
class Circle : public Shape
{
public:
Circle() {}
~Circle(){
cout << "this is ~Circle()" << endl;
}
void draw(){
//do something
cout << "draw Circle.." << endl;
}
};
class Triangle : public Shape
{
public:
Triangle() {}
~Triangle(){
cout << "this is ~Triangle()" << endl;
}
void draw(){
//do something
cout << "draw Triangle.." << endl;
}
};

int main()
{
Shape * pRectange = new Rectange();
Shape * pCircle = new Circle();
Shape * pTriangle = new Triangle();

pRectange->draw();
pCircle->draw();
pTriangle->draw();

delete pRectange;
pRectange = 0;
delete pCircle;
pCircle = 0;
delete pTriangle;
pTriangle = 0;

return 0;
}

输出如下:

draw rectange..
draw Circle..
draw Triangle..
this is ~Rectange()
this is ~Shape()
this is ~Circle()
this is ~Shape()
this is ~Triangle()
this is ~Shape()

所以,任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数。

不要误用虚析构函数

刚刚我们看到,任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数。那么如果 class 不含 virtual 函数,通常表示它并不意图被用做一个 base class。当class 不企图被当做基类,让析构函数声明为 virtual 往往是个馊主意。

想要实现虚函数,对象必须携带某些信息,主要用来在运行期决定哪一个 virtual 函数应该被调用。这份信息通畅是由一个所谓的 vptr(virtual table pointer)指针指出。vptr 指向一个函数指针构成的数组,称为 vtbl(virtual table),每一个带有 virtual 函数的 class 都有一个对应的 vtbl,当对象调用某一个 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的那个 vtbl----编译器在其中寻找适当的函数指针。

所以说,如果给一个没有虚函数的基类声明析构函数为虚析构函数,其对象体积会增加,导致空间浪费。