# 为什么 C++ 基类析构函数需要是虚函数?

面试高频指数:★★★☆☆

# 析构函数作用

首先我们需要知道析构函数的作用是什么。

析构函数是进行类的清理工作,比如释放内存、关闭DB链接、关闭Socket等等,

前面我们在介绍虚函数的时候就说到,为实现多态性(C++多态 (opens new window)),可以通过基类的指针或引用访问派生类的成员。

也就是说,声明一个基类指针,这个基类指针可以指向派生类对象。

# 举例

下面我们来看一个例子:

#include <iostream>

class Base {
public:
    // 注意,这里的析构函数没有定义为虚函数
    ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        resource = new int[100]; // 分配资源
    }

    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
        delete[] resource; // 释放资源
    }

private:
    int* resource; // 存储资源的指针
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 只会调用Base的析构函数,Derived的析构函数不会被调用
    return 0;
}

这个例子执行的输出结果为:

Base destructor called.

由于基类Base的析构函数没有定义为虚函数,当创建一个派生类Derived的对象,并通过基类指针ptr删除它时,只有基类Base的析构函数被调用(因为这里没有多态,构造多态的必要条件就是虚函数)。

派生类Derived的析构函数不会被调用,导致资源(这里是resource)没有被释放,从而产生资源泄漏。

再来看一个将基类定义为虚函数的例子:

class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 调用Derived的析构函数,然后调用Base的析构函数
    return 0;
}

输出结果为:

Derived destructor called.
Base destructor called.

在这个例子中,基类Base的析构函数是虚函数,所以当删除ptr时,会首先调用派生类Derived的析构函数,然后调用基类Base的析构函数,这样可以确保对象被正确销毁。

# 为什么默认的析构函数不是虚函数?

那么既然基类的析构函数如此有必要被定义成虚函数,为何类的默认析构函数却是非虚函数呢?

首先一点,语言设计者如此设计,肯定是有道理的。

原来是因为,虚函数不同于普通成员函数,当类中有虚成员函数时,类会自动进行一些额外工作。

这些额外的工作包括生成虚函数表和虚表指针,虚表指针指向虚函数表。

每个类都有自己的虚函数表,虚函数表的作用就是保存本类中虚函数的地址,我们可以把虚函数表形象地看成一个数组,这个数组的每个元素存放的就是各个虚函数的地址。(关于虚函数表可以看这篇文章虚函数 (opens new window)

这样一来,就会占用额外的内存,当们定义的类不被其他类继承时,这种内存开销无疑是浪费的。

这样一说,问题就不言而喻了。当我们创建一个类时,系统默认我们不会将该类作为基类,所以就将默认的析构函数定义成非虚函数,这样就不会占用额外的内存空间。

同时,系统也相信程序开发者在定义一个基类时,会显示地将基类的析构函数定义成虚函数,此时该类才会维护虚函数表和虚表指针。

# 零成本抽象原则

这也就是 C++ 的一个设计哲学:zero overhead abstraction

我的理解,所谓「零成本抽象」有两个层面的意思。

  1. 不需要为没有使用到的语言特性付出代价。
  2. 使用某种语言特性,不会带来运行时的代价。

放在这个地方就是,如果我们知道一个类不会被其它类继承,那么也就没必要将析构函数设置为虚函数,因为一旦引入虚函数就会引入虚表机制,这会造成额外的开销。

# 总结

在C++中,基类的析构函数需要定义为虚函数,以确保在通过基类指针或引用删除派生类对象时,能够正确地调用派生类的析构函数,否则派生类的析构函数不会被调用,这部分资源也就并无法被释放。

将基类的析构函数定义为虚函数后,C++运行时会自动识别指向的对象的实际类型,并确保调用正确的派生类析构函数。

当派生类析构函数执行完毕后,基类的析构函数也会自动调用,以确保对象完全销毁。

参考 https://blog.csdn.net/qq_42247231/article/details/105109709

最新原创的文章都先发布在公众号,欢迎关注哦~,
扫描下方二维码回复「CS」可以获得我汇总整理的计算机学习资料~

编程指北图片
@2021-2024 编程指北 版权所有 粤ICP备2021169086号-2