本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
微信搜索“编程笔记本”,获取更多信息
------------- codingbook2020 -------------
今天我们来谈一谈面试 C++ 工程师时经常被谈到的一个问题:为什么析构函数必须是虚函数?为什么默认的析构函数不是虚函数?
首先,我们看一下百度百科对虚函数是怎么定义的:
在某基类中声明为 virtual
并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名 ( 参数表 ) { 函数体 }
;实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
好了,现在我们大概知道什么是虚函数,虚函数就是类中使用关键 virtual
修饰的成员函数,其目的是为了实现多态性。
那么什么是多态性呢?
所谓多态性,顾名思义就是“多个性态”。更具体一点的就是,用一个名字定义多个函数,这些函数执行不同但相似的工作。最简单的多态性的实现方式就是函数重载和模板,这两种属于静态多态性。还有一种是动态多态性,其实现方式就是我们今天要说的虚函数。
回归正题。
一、为什么析构函数必须是虚函数?
当然了,这么说其实是不太严谨的,因为我完全可以将析构函数定义成非虚函数。这个我们后面再说。
首先我们需要知道析构函数的作用是什么。析构函数是进行类的清理工作,具体来说就是释放构造函数开辟的内存空间和资源,当然我们完全可以在析构函数中进行任何我们想要的操作,比如下面我们给出的示例代码,就在析构函数中打印提示信息。
前面我们在介绍虚函数的时候就说到,为实现多态性,可以通过基类的指针或引用访问派生类的成员。也就是说,声明一个基类指针,这个基类指针可以指向派生类对象。
下面我们来看一个例子:
#include <iostream>
using namespace std;
class Father {
public:
~Father() {
cout << "class Father destroyed" << endl;
}
};
class Son : public Father {
public:
~Son() {
cout << "class Son destroyed" << endl;
}
};
int main() {
Father* p = new Son;
delete p;
return 0;
}
/*
运行结果:
class Father destroyed
*/
上面的示例程序中,我们定义了两个类,一个基类,一个派生类,派生类公有继承父类。为了描述简单,这两个类只定义了析构函数,并在析构函数中输出提示信息。在主函数中,我们声明了一个基类的指针,并用一个派生类的实例去初始化这个基类指针,随后删除这个指针。我们看到程序运行的结果,只有基类的析构函数被调用。
为什么会这样呢?指针明明指向的是派生类对象,那删除这个指针,为何只有基类的析构函数被调用,而派生类的析构函数却没有调用呢?
我们先把问题留在这里,接下来我们看看,若析构函数被定义成虚函数会怎么样呢?
#include <iostream>
using namespace std;
class Father {
public:
virtual ~Father() {
cout << "class Father destroyed" << endl;
}
};
class Son : public Father {
public:
~Son() {
cout << "class Son destroyed" << endl;
}
};
int main() {
Father* p = new Son;
delete p;
return 0;
}
/*
运行结果:
class Son destroyed
class Father destroyed
*/
当基类的析构函数被定义成虚函数时,我们再来删除这个指针时,先调用派生类的析构函数,再调用基类的析构函数,很明显这才是我们想要的结果。因为指针指向的是一个派生类实例,我们销毁这个实例时,肯定是希望即清理派生类自己的资源,同时又清理从基类继承过来的资源。而当基类的析构函数为非虚函数时,删除一个基类指针指向的派生类实例时,只清理了派生类从基类继承过来的资源,而派生类自己独有的资源却没有被清理,这显然不是我们希望的。
所以说,如果一个类会被其他类继承,那么我们有必要将被继承的类(基类)的析构函数定义成虚函数。这样,释放基类指针指向的派生类实例时,清理工作才能全面进行,才不会发生内存泄漏。
二、为什么默认的析构函数不是虚函数?
那么既然基类的析构函数如此有必要被定义成虚函数,为何类的默认析构函数却是非虚函数呢?
首先一点,语言设计者如此设计,肯定是有道理的。
原来是因为,虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作。这些额外的工作包括生成虚函数表和虚表指针,虚表指针指向虚函数表。每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。
这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的。
这样一说,问题就不言而喻了。当我们创建一个类时,系统默认我们不会将该类作为基类,所以就将默认的析构函数定义成非虚函数,这样就不会占用额外的内存空间。同时,系统也相信程序开发者在定义一个基类时,会显示地将基类的析构函数定义成虚函数,此时该类才会维护虚函数表和虚表指针。
这个问题至此就解释完成了,祝大家面试顺利!
本博客可能随时删除或隐藏,请关注微信公众号,获取永久内容。
识别下方二维码关注我,或微信搜索**“编程笔记本”**,获取更多信息。