C++是门神奇的语言,总是有一些不能以我们“程序员”编写它的角度去理解的问题,究其根本,大多都是C++编译器在“适当”的时候,会自动为我们的程序增加或删除一些东西,如果其编译器所做的改动,与我们预期的结果并不相关,自然我们也能得到较好理解的运行结果,然而,深入学习C++之后,就会发现,其中很多特性,必须要掌握,必须要知道编译器做了哪些东西,才能保证程序的正确执行。
C++类的构造,析构函数是C++编译器在编译的过程中,所作“适当”修改的主要场所
不扯远,在类继承中,一般类的析构函数都要被定义成虚函数,或者必须要定义成虚函数,这是为什么呢?
看如下两个类:
|
|
这上面Point3d
是由基类Point2d
派生出来的,C++有个特点,可以用基类的指针指向派生类,即派生类可以用基类指针new
出来
|
|
至于为什么能这样new
,我想应该是new
出来的空间比基类指针所能访问到的空间更大,所以认为是“安全”的,基于这个理念,派生类指针是不能new
基类的,因为会产生“不安全”的访问。
通过看了《深度探索C++对象模型》,来解释一下上述一行式子中,编译器究竟做了什么事:
1,产生一个指向Point2d
的指针,pt2d
;
2,生成一个Point3d
的类;
3,将Point3d
产生的地址赋值给一个指向Point2d
的指针
以上的解释比较直白,甚至可以说是“无用”的
将它们细化,第一步没什么好说的,第二步中,生成一个Point3d
的类,派生类中的构造函数是如何执行呢?分为以下几步:
1)先调用基类的构造函数,其实在编译器中,是将基类的构造函数嵌入到派生类的构造函数中;
2)如果有虚函数表,将虚函数表重新绑定;
3)执行当前的构造函数,产生一个派生类。
为了验证这一过程,可以在构造函数写出输出函数,看是否这样,如下代码:
|
|
可见,输出是
|
|
将第二步分解了,那么它又是如何执行第三步呢?
由第二步知道,在派生类中,包含着一个已经构造出来的基类,这个基类在原来的基础上,只是虚函数表做了更改,重新绑定了派生类的函数。而这个Point2d
的指针,就是指向派生类的基类实体。
如本文标题所写,要讨论的是为什么是虚析构函数?
在上文中的那段完整可执行代码中,如果你在析构函数也添加打印信息,如下:
|
|
重新运行上述代码,则发现delete
操作只促发了基类的的析构函数,并未调用派生类的析构函数,于是就产生了问题!
众所周知,析构函数的作用,就是在销毁对象时“合适”的处理一些信息,其实说白了就是擦屁股。
而虚函数的作用,就是可以动态绑定,比如我在基类定义了一个虚函数virtual show()
,那么如果我在派生类重写了这个函数,则这时用上述基类指针new
出派生类来之后,pt2d
访问show
函数,实际是派生类的,为何?因为在构造的时候进行重新绑定,按照这个思想,我们应该对基类的析构函数定义为虚函数,这样delete
的时候,就是调用派生类的析构函数了。
于是,理所当然,如下:
|
|
输出是:
|
|
哇,它正常先调用了派生类的析构函数,再调用基类的析构函数,完全销毁了对象,没有内存泄漏之类的问题诶!
但是,我当时就思考一个问题,如果是虚函数,一般情况下,派生类重写了虚函数,则它只调用派生类的虚函数,而不调用基类的虚函数,但这里却调用了基类的虚函数,这是为什么呢?
记得前面讲了构造函数的生成对象的3个主要步骤,通过阅读《深度探索C++对象模型》就能够了解到,析构函数的行为,在顺序上,是与构造函数相反的,即:
1)执行当前析构函数;
2)重新解绑定虚函数表;
3)调用基类的析构函数
这里最重要的是2,它重新解绑定了虚函数表,这样派生类的析构函数才能找到基类的析构函数,从而能够正确完成程序的功能!
听起来很奇妙!
实际中,基类(如果有派生类)的析构函数往往都定义成虚函数。
接下来就还有个小问题:为何构造函数不能定义为虚函数呢?
前面提到过,构造函数有个过程是绑定虚函数表,如果构造函数定义为虚函数,这个虚函数表该如何生成?如何绑定?对象还没完成构造,如何动态绑定虚函数表!
以上是个人觉得对虚析构函数机制的深入了解。