25-4 虚构析构函数、虚构赋值与覆盖虚拟化
虚析构函数
尽管C++会在您未自行提供析构函数时为类提供默认析构函数,但有时您仍需自定义析构函数(尤其当类需要释放内存时)。若涉及继承,务必将析构函数声明为虚函数。请看以下示例:
#include <iostream>
class Base
{
public:
~Base() // note: not virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived: public Base
{
private:
int* m_array {};
public:
Derived(int length)
: m_array{ new int[length] }
{
}
~Derived() // note: not virtual (your compiler may warn you about this)
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived* derived { new Derived(5) };
Base* base { derived };
delete base;
return 0;
}
注意:若编译上述示例,编译器可能会对非虚构造函数发出警告(此为本示例的刻意设计)。您可能需要禁用将警告视为错误的编译器选项才能继续。
由于 base 是 Base 指针,当 base 被删除时,程序会检查 Base 析构函数是否为虚函数。由于它不是虚函数,程序便认为只需调用 Base 析构函数即可。这可从上述示例的输出中得到验证:
(这里注意,将警告视为错误的编译器编译不会有提示,例如非虚警告,有时候放松可能会检查出问题好在有内存泄漏工具兜底)


然而,我们确实希望删除函数能调用派生类的析构函数(该析构函数将依次调用基类的析构函数),否则 m_array 将不会被删除。为此,我们通过将基类的析构函数设为虚函数来实现:
#include <iostream>
class Base
{
public:
virtual ~Base() // note: virtual
{
std::cout << "Calling ~Base()\n";
}
};
class Derived: public Base
{
private:
int* m_array {};
public:
Derived(int length)
: m_array{ new int[length] }
{
}
virtual ~Derived() // note: virtual
{
std::cout << "Calling ~Derived()\n";
delete[] m_array;
}
};
int main()
{
Derived* derived { new Derived(5) };
Base* base { derived };
delete base;
return 0;
}
现在这个程序产生以下结果:

规则:
在处理继承时,应将所有显式析构函数声明为虚函数。
与普通虚成员函数类似,若基类函数为虚函数,则所有派生类的重写函数均会被视为虚函数,无论是否显式声明。无需为派生类创建空析构函数来标记其虚性。
需注意:若需使基类拥有空虚析构函数,可按以下方式定义:
virtual ~Base() = default; // generate a virtual default destructor
虚拟赋值运算符
赋值运算符可以被声明为虚函数。然而,与虚构造函数始终值得采用的情况不同,赋值运算符的虚化会引发诸多复杂问题,涉及超出本教程范围的高级主题。因此,为保持简洁性,我们建议您暂时保持赋值运算符的非虚属性。
忽略虚拟化
极少数情况下,您可能需要忽略函数的虚拟化。例如,请看以下代码:
#include <string_view>
class Base
{
public:
virtual ~Base() = default;
virtual std::string_view getName() const { return "Base"; }
};
class Derived: public Base
{
public:
virtual std::string_view getName() const { return "Derived"; }
};
在某些情况下,您可能希望基类指针指向派生对象时调用基类的 getName() 方法而非派生类的 getName() 方法。要实现此功能,只需使用作用域解析运算符:
#include <iostream>
int main()
{
Derived derived {};
const Base& base { derived };
// Calls Base::getName() instead of the virtualized Derived::getName()
std::cout << base.Base::getName() << '\n';
return 0;
}
你可能不会经常用到这个功能,但至少知道它存在也是件好事。
是否应该将所有析构函数设为虚函数?
这是新手程序员常问的问题。如开头示例所示,若基类析构函数未标记为虚函数,当程序员后续删除指向派生对象的基类指针时,程序将面临内存泄漏风险。避免此问题的方案之一是将所有析构函数设为虚函数。但真的应该这样做吗?
简单回答“应该”很轻松——这样就能让任何类都充当基类。但此举会带来性能开销(每个实例都需添加虚拟指针)。因此需权衡成本与设计意图。
我们建议如下:若类未被明确设计为基类,通常不应包含虚成员或虚析构函数,仍可通过组合模式使用该类。若类被设计为基类且/或包含虚函数,则必须始终提供虚析构函数。
若决定使类不可继承,则需考虑如何强制执行。
传统智慧(最初由备受推崇的C++大师Herb Sutter提出)建议通过以下方式避免非虚构造函数导致的内存泄漏:“基类构造函数应为public且虚,或protected且非虚。” 基类若采用受保护的析构函数,则无法通过基类指针进行删除操作,从而阻止了通过基类指针删除派生类对象的可能性。
遗憾的是,此方案也阻断了公共域对基类析构函数的调用。这意味着:
- 我们不应动态分配基类对象,因为缺乏常规删除机制(虽有非常规变通方案,但令人不适)。
- 甚至无法静态分配基类对象,因为当它们作用域结束时无法访问析构函数。
换言之,采用此方法使派生类安全的同时,基类本身几乎无法独立使用。
随着 final 修饰符引入语言,我们的建议如下:
- 若计划让类被继承,请确保析构函数为虚函数且公开。
- 若不允许类被继承,请标记为 final。此举将彻底阻止其他类继承,且不会对该类本身施加任何使用限制。

浙公网安备 33010602011771号