如果类没有定义析构函数,那么只有在类自带的member object(或者类自己的base class)拥有析构函数的情况下,编译器才会自动合成出一个来。否则,析构函数会被视为不需要,也就不需要被合成,更不需要被调用。
比如说下面的Point,默认情况下并没有被编译器合成出一个析构函数—甚至虽然它拥有一个虚函数:
class Point{
public:
Point(float x = 0.0, float y = 0.0)
: _x(x), _y(y){}
Point(const Point&);
Point& operator=(const Point&);
virtual ~Point();
virtual float z() {return 0.0;};
protected:
float _x, _y;
};
为了决定一个类是否需要一个程序层面的析构(或者构造),请你想象一个类对象的声明式从哪里结束(或者开始)?需要什么操作才能保证对象的完成?这也是析构函数和构造函数什么时候起作用的关键。举个例子:
{
Point pt;
Point *p = new Point3d;
foo(&pt, p);
delete p;
}
我们可以看到,pt和p在作为foo()函数的参数之前,都必须先初始化为某些坐标值。这时候需要一个构造函数,否则使用者必须明确的提供坐标值。一般来说,类的使用者没版本检验一个local变量或者heap变量以知道它们是否被初始化。如果没有构造函数,抽象化的实用就会有错误的倾向。即构造函数是必要的
那当我们明确的delet掉p的时候会是怎么样呢?有任何程序上必须处理的吗?是否需要在delete之前这么做:
p->x(0);
p->y(0);
不,当然不需要。没有任何理由说明在delet一个对象之前现需要将内容清除干净,也不需要归还任何资源。在结束pt和p的声明之前,没有任何类使用者层面的程序操作是绝对必要的。因此,也就不一定需要一个析构函数。
而对于Vertex类,我们必须提供一个Vertex析构函数,以删除链表空间
对于派生自Vertex和Point3d的Vertex3d,如果我们不提供一个显式Vertex3d析构函数,编译器就必须合成一个Vertex3d析构函数,其唯一任务就是调用Vertex析构函数。如果我们提供一个Vertex3d析构函数,编译器会扩展它,使它调用Vertex析构函数(在我们所提供的程序代码之后)。一个由程序员定义的析构函数被扩展的方式类似构造函数被扩展的方式,但顺序相反:
- 析构函数本身被执行
- 如果类拥有member class objects,而后者拥有析构函数,那么它们会以声明顺序的相反顺序被调用
- 如果object内带一个
vptr
,那么首先现在被重新指定,指向适当的基类的虚函数 - 如果有任何直接的(上一层)nonvirtual base classes拥有destructor,它们会以其声明相反顺序被调用
- 如果有任何virtual base classes拥有destructor,而当前讨论的这个类是最尾端(most-derived)的类,那么它们会以原来的构造顺序相反的顺序被调用
就像构造函数一样,目前对于析构函数的一种最佳实现策略是维护两份析构函数实体:
- 一个complete object实体,总是设定好vptrs,并调用virtual base class destructors
- 一个base class subobject实体:除非在析构函数中调用一个virtual function,否则它绝不会调用virtual base class destructors并设定vptr
一个object的生命结束于其destructor开始执行之时。由于每一个base class destructor都轮番被调用,所以派生类实际上变成了一个完整的object。比如一个PVertex对象归还其内存空间之前,会依次变成一个Vertex3d对象、一个Vertex对象、一个Point3d对象,最后变成Point对象。当我们在析构函数中调用成员函数时,对象的蜕变会因为vptr的重新设定而受到影响