虚析构函数的场景

如果这个类不作为任何类的基类,析构函数是否为虚函数并不重要,因为它不必承担回收派生类资源的责任。那什么情况下给析构函数声明为虚函数是必要的?

通过基类的指针来删除派生类的对象时,基类的析构函数应该是虚函数

会被继承但不需要虚析构函数

1
2
3
4
5
6
7
class NonCopyable {
protected:
NonCopyable(const NonCopyable &) = delete; // 阻止拷贝
NonCopyable &operator=(const NonCopyable &) = delete; // 阻止赋值
NonCopyable() = default;
~NonCopyable() = default;
};

任何需要防止被拷贝和赋值都需要删除拷贝构造函数和赋值运算符函数,为了方便,继承 NonCopyable类 即可。既然这个类就是用来给其它类继承,为何却没有把析构函数声明为虚函数呢?因为我们不会通过 NonCopyable类 来创建对象,仅仅只是提供阻止拷贝和阻止赋值的功能给到派生类。

会被继承且需要虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Derived : public NonCopyable {
public:
virtual ~Derived() { // 添加虚析构函数
// Derived 的清理代码
}
void doSomething() {
// 示例方法
}
};

class MoreDerived : public Derived {
public:
~MoreDerived() override {
// MoreDerived 的清理代码
}
};

Derived 类是 NonCopyable 的派生类,是 MoreDerived 的基类。我们前面讲过 NonCopyable 类不可能用来创建对象,现在Derived 类作为MoreDerived 的基类,当Derived 类作为MoreDerived 类对象的指针的时候,Derived 类就需要承担回收MoreDerived 类对象资源的责任(调用MoreDerived 类对象的析构函数)。基于上述分析,我们需要把Derived 类的析构函数声明为虚函数。

析构函数的调用顺序

当删除一个基类指针指向的派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数。这是为了确保派生类的资源先被正确释放,然后再释放基类的资源。

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
class Base {
public:
virtual void func() {
std::cout << "Base::func" << std::endl;
}
virtual ~Base() {
std::cout << "Base destructor" << std::endl;
}
};

class Derived : public Base {
public:
void func() override {
std::cout << "Derived::func" << std::endl;
}
~Derived() override {
std::cout << "Derived destructor" << std::endl;
}
};

int main() {
Base* ptr = new Derived();
ptr->func(); // 调用 Derived::func
delete ptr; // 确保调用 Derived 析构函数,然后调用 Base 析构函数
return 0;
}

那我们就要提出一些有趣的问题:

通过基类创建的派生类对象究竟是基类对象还是派生类对象?是派生类对象。

既然是创建的派生类对象为什么需要调用基类的析构函数?虽然基类没有创建对象,但派生类对象不仅包含派生类的成员,还包含基类的成员。基类的成员是派生类对象的一部分,因此在创建派生类对象时,实际上包含了两个部分:基类部分和派生类部分。基类的成员只能由基类清理,不能由派生类清理。派生类的成员也只能由派生类清理,不能由基类清理。但是将基类的析构声明为虚函数会调用派生类的析构函数,否则只会调用自己的析构函数,从而造成派生类对象的内存泄漏。

最后的话

只要弄清楚什么情况下把析构函数声明为虚函数,才不会盲目给任何类的析构函数声明为虚函数。因为虚函数会创建虚函数表,这个不必要的开销能避免就要避免。