request-free-img

虚析构函数的作用详解:为什么基类析构函数需要声明为virtual?

虚析构函数在C++中的重要性

虚析构函数在 C++ 中是非常基础但又很关键的概念,但又让很多新手感到困惑。

今天这个视频就讲一讲虚析构函数的作用。最后也会帮助大家复习一下虚函数表的原理,并顺便给大家讲讲虚析构函数在实际使用中应该注意的事项。

如果析构函数不是虚的会发生什么问题?

要理解虚析构函数的作用,其实我们只需要弄清楚如果析构函数不是虚的会存在什么问题就行了。

#include <iostream>

class Base {
public:
// 动态分配的内存
int* data;
Base() {
// 动态分配内存
data = new int[10];
std::cout << "基类构造函数:分配内存" << std::endl;
}
~Base() {
delete[]data;
std::cout << "基类析构函数" << std::endl;
}
};

class Derived : public Base {
public:
// 派生类中额外的动态分配内存
int* extraData;
Derived() {
// 动态分配内存
extraData = new int[5];
std::cout << "子类构造函数:分配内存" << std::endl;
}
~Derived() {
delete []extraData;
std::cout << "子类析构函数" << std::endl;
}
};

int main()
{
Base* basePtr = new Derived(); // 使用基类指针指向派生类对象
delete basePtr; // 销毁对象
return 1;
}

代码运行结果分析

咱们看代码。我们有一个叫Base的基类,它在构造函数中会动态分配内存,然后在析构函数中会释放所分配的内存。但是请注意,这个Base的基类析构函数不是虚的。

然后我们有一个叫Derived的子类,它继承自Base类。在它的构造函数中也分配了一块内存,同样在它的析构函数中也会释放所分配的内存。

然后我们在main函数中创建了一个子类Derived的对象并把它赋值给基类Base 的指针basePtr,然后用delete释放这个对象所占用的内存。

正常情况下只要我们基类和子类的析构函数能正常被调用,就不会存在内存泄露。

非虚析构函数导致的问题

我们运行这份代码看看。可以看到,首先调用了基类的构造函数并分配了内存,然后调用了子类的构造函数也分配了内存。

最后在释放的时候,却只调用了基类的析构函数。

这就是非虚析构函数的问题,简单来说,虚析构函数用于确保派生类的析构函数被正确调用,防止资源泄露。

将析构函数声明为virtual后的效果

现在我们把基类的析构函数改成虚的:

virtual ~Base() {
delete[]data;
std::cout << "基类析构函数" << std::endl;
}

再运行程序,看到了吗,现在子类的析构函数也被正确调用了。

虚函数表原理回顾

那为什么会这样呢?如果你看过我之前关于虚函数表的视频,那你一定知道它的原理。

这次我们再简单复习一下:C++ 中的 虚函数 是通过 虚函数表(vtable) 来实现的。

当base类的存在虚函数时,编译器会为它创建一个vptr用于存储类中的虚函数指针。

在我们的示例代码中,子类Derived会继承基类的虚函数表,并将基类虚函数的指针替换成子类中重写的函数指针。

也就是因为析构函数是虚的,所以Derived类会用自己的析构函数指针替换基类的函数表中的指针。这样子类中vptr指向的析构函数就是子类的了,从而在释放时正确的调用子类的析构函数。

使用虚析构函数的注意事项

最后,析构函数是不能有参数,也不允许被重载的。

虽然虚析构函数在多态中是必要的,但是虚析构函数会触发虚函数表的查找,这会增加一定的时间开销。不过,在大多数情况下,虚析构函数的好处远大于它带来的性能开销。

怎么样,现在你明白了吗?


更多问题探讨,请关注公众号:程序员角