C++的虚析构函数

C++是门神奇的语言,总是有一些不能以我们“程序员”编写它的角度去理解的问题,究其根本,大多都是C++编译器在“适当”的时候,会自动为我们的程序增加或删除一些东西,如果其编译器所做的改动,与我们预期的结果并不相关,自然我们也能得到较好理解的运行结果,然而,深入学习C++之后,就会发现,其中很多特性,必须要掌握,必须要知道编译器做了哪些东西,才能保证程序的正确执行。

C++类的构造,析构函数是C++编译器在编译的过程中,所作“适当”修改的主要场所

不扯远,在类继承中,一般类的析构函数都要被定义成虚函数,或者必须要定义成虚函数,这是为什么呢?

看如下两个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Point2d
{
public:
Point2d():x(1),y(2) {}
~Point2d(){}
int x;
int y;
};
class Point3d : public Point2d
{
public:
Point3d():z(3){}
~Point3d(){}
int z;
};

这上面Point3d是由基类Point2d派生出来的,C++有个特点,可以用基类的指针指向派生类,即派生类可以用基类指针new出来

1
Point2d* pt2d=new Point3d();

至于为什么能这样new,我想应该是new出来的空间比基类指针所能访问到的空间更大,所以认为是“安全”的,基于这个理念,派生类指针是不能new基类的,因为会产生“不安全”的访问。

通过看了《深度探索C++对象模型》,来解释一下上述一行式子中,编译器究竟做了什么事:

1,产生一个指向Point2d的指针,pt2d;
2,生成一个Point3d的类;
3,将Point3d产生的地址赋值给一个指向Point2d的指针

以上的解释比较直白,甚至可以说是“无用”的

将它们细化,第一步没什么好说的,第二步中,生成一个Point3d的类,派生类中的构造函数是如何执行呢?分为以下几步:

1)先调用基类的构造函数,其实在编译器中,是将基类的构造函数嵌入到派生类的构造函数中;

2)如果有虚函数表,将虚函数表重新绑定;

3)执行当前的构造函数,产生一个派生类。

为了验证这一过程,可以在构造函数写出输出函数,看是否这样,如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*
* con_des.cpp
* Copyright (C) 2013 moondark <liaoxl2012@gmail.com>
*
* Distributed under terms of the MIT license.
*/
#include <iostream>
using namespace std;
class Point2d
{
public:
Point2d():x(1),y(2)
{
cout << "Point2d Constructor!" << endl;
}
~Point2d(){}
int x;
int y;
};
class Point3d : public Point2d
{
public:
Point3d():z(3)
{
cout << "Point3d Constructor!" << endl;
}
~Point3d(){}
int z;
};
int main(int argc, char* argv[])
{
Point2d* pt2d=new Point3d();
delete pt2d;
return 0;
}

可见,输出是

1
2
Point2d Constructor!
Point3d Constructor!

将第二步分解了,那么它又是如何执行第三步呢?

由第二步知道,在派生类中,包含着一个已经构造出来的基类,这个基类在原来的基础上,只是虚函数表做了更改,重新绑定了派生类的函数。而这个Point2d的指针,就是指向派生类的基类实体。

如本文标题所写,要讨论的是为什么是虚析构函数?

在上文中的那段完整可执行代码中,如果你在析构函数也添加打印信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Point2d
{
public:
Point2d():x(1),y(2)
{
cout << "Point2d Constructor!" << endl;
}
~Point2d()
{
cout << "Point2d Destructor!" << endl;
}
int x;
int y;
};
class Point3d : public Point2d
{
public:
Point3d():z(3)
{
cout << "Point3d Constructor!" << endl;
}
~Point3d()
{
cout << "Point3d Destructor!" << endl;
}
int z;
};

重新运行上述代码,则发现delete操作只促发了基类的的析构函数,并未调用派生类的析构函数,于是就产生了问题!

众所周知,析构函数的作用,就是在销毁对象时“合适”的处理一些信息,其实说白了就是擦屁股。

而虚函数的作用,就是可以动态绑定,比如我在基类定义了一个虚函数virtual show(),那么如果我在派生类重写了这个函数,则这时用上述基类指针new出派生类来之后,pt2d访问show函数,实际是派生类的,为何?因为在构造的时候进行重新绑定,按照这个思想,我们应该对基类的析构函数定义为虚函数,这样delete的时候,就是调用派生类的析构函数了。

于是,理所当然,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Point2d
{
public:
Point2d():x(1),y(2)
{
cout << "Point2d Constructor!" << endl;
}
virtual ~Point2d()
{
cout << "Point2d Destructor!" << endl;
}
int x;
int y;
};
class Point3d : public Point2d
{
public:
Point3d():z(3)
{
cout << "Point3d Constructor!" << endl;
}
~Point3d()
{
cout << "Point3d Destructor!" << endl;
}
int z;
};

输出是:

1
2
3
4
Point2d Constructor!
Point3d Constructor!
Point3d Destructor!
Point2d Destructor!

哇,它正常先调用了派生类的析构函数,再调用基类的析构函数,完全销毁了对象,没有内存泄漏之类的问题诶!

但是,我当时就思考一个问题,如果是虚函数,一般情况下,派生类重写了虚函数,则它只调用派生类的虚函数,而不调用基类的虚函数,但这里却调用了基类的虚函数,这是为什么呢?

记得前面讲了构造函数的生成对象的3个主要步骤,通过阅读《深度探索C++对象模型》就能够了解到,析构函数的行为,在顺序上,是与构造函数相反的,即:

1)执行当前析构函数;

2)重新解绑定虚函数表;

3)调用基类的析构函数

这里最重要的是2,它重新解绑定了虚函数表,这样派生类的析构函数才能找到基类的析构函数,从而能够正确完成程序的功能!

听起来很奇妙!

实际中,基类(如果有派生类)的析构函数往往都定义成虚函数。

接下来就还有个小问题:为何构造函数不能定义为虚函数呢?

前面提到过,构造函数有个过程是绑定虚函数表,如果构造函数定义为虚函数,这个虚函数表该如何生成?如何绑定?对象还没完成构造,如何动态绑定虚函数表!

以上是个人觉得对虚析构函数机制的深入了解。