Namespaces
Variants

Destructors

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

析构函数是一种特殊的 成员函数 ,当 对象的生命周期 结束时被调用。析构函数的目的是释放对象在其生命周期内可能获取的资源。

析构函数不能是 协程

(since C++20)

目录

语法

析构函数 (C++20 前) 预期析构函数 (C++20 起) 使用以下形式的成员 函数声明符 进行声明:

类名带波浪号 ( 参数列表  (可选) ) 异常说明  (可选) 属性  (可选)
class-name-with-tilde - 一个 标识符表达式 可能后跟一组 属性 ,且 (自 C++11 起) 可能被一对圆括号包围
parameter-list - 形参列表 (必须为空或为 void
except -

动态异常规范

(直至 C++11)

可为 动态异常规范
noexcept 规范

(自 C++11 起)
(直至 C++17)

noexcept 规范

(自 C++17 起)
attr - (自 C++11 起) 一组 属性

预期 (C++20 起) 析构函数声明的 声明说明符 中,唯一允许的说明符是 constexpr (C++11 起) friend inline virtual (特别地,不允许出现返回类型)。

标识符表达式 class-name-with-tilde 必须具有以下形式之一:

  • 对于类,标识符表达式为 ~ 后紧跟直接外围类的 注入类名
  • 对于类模板,标识符表达式为 ~ 后紧跟 命名 当前实例化 的类名 (C++20 前) 直接外围类模板的注入类名 (C++20 起)
  • 否则,该标识符表达式是一个限定标识符,其终端非限定标识符由 ~ 后接由该限定标识符非终端部分所指定类的注入类名组成。

说明

析构函数在对象 生命周期 结束时被隐式调用,包括

  • 线程退出时,对于具有线程局部存储期的对象
(since C++11)
  • 作用域结束,适用于具有自动存储期的对象以及通过绑定到引用而延长生命期的临时对象
  • delete 表达式 ,适用于具有动态存储期的对象
  • 完整 表达式 结束,适用于无名临时对象
  • 栈展开 ,当异常逃逸其代码块且未被捕获时,适用于具有自动存储期的对象

析构函数也可以被显式调用。

候选析构函数

一个类可以拥有一个或多个候选析构函数,其中将被选定一个作为该类的析构函数。

为了确定哪个候选析构函数是最终的析构函数,在类定义结束时,将在类中声明的候选析构函数之间执行 重载决议 ,参数列表为空。如果重载决议失败,则程序非良构。析构函数的选择不会 ODR 使用 被选中的析构函数,且被选中的析构函数可以是已删除的。

所有候选析构函数都是特殊成员函数。如果类 T 没有用户声明的候选析构函数,编译器将始终 隐式声明 一个,且该隐式声明的候选析构函数也是 T 的析构函数。

#include <cstdio>
#include <type_traits>
template<typename T>
struct A
{
    ~A() requires std::is_integral_v<T> { std::puts("~A, T is integral"); }
    ~A() requires std::is_pointer_v<T> { std::puts("~A, T is a pointer"); }
    ~A() { std::puts("~A, T is anything else"); }
};
int main()
{
    A<int> a;
    A<int*> b;
    A<float> c;
}

输出:

~A, T is anything else
~A, T is a pointer
~A, T is integral
(自 C++20 起)

可能被调用的析构函数

T 的析构函数在以下情况下 可能被调用

如果可能被调用的析构函数是 被删除或 (since C++11) 在调用上下文中不可访问,则程序非良构。

隐式声明的析构函数

如果类类型没有提供用户声明的 预期 (since C++20) 析构函数,编译器将始终声明一个析构函数作为其类的 inline public 成员。

与任何隐式声明的特殊成员函数一样,隐式声明的析构函数的异常规范是 不抛出异常的 ,除非 任何潜在构造的基类或成员的析构函数是 潜在抛出的 (C++17 起) 隐式定义会直接调用具有不同异常规范的函数 (C++17 前) 。实际上,隐式析构函数都是 noexcept 的,除非类被具有 noexcept ( false ) 析构函数的基类或成员"污染"。

隐式定义的析构函数

如果隐式声明的析构函数未被删除,则当编译器 ODR使用 时会隐式定义(即生成并编译函数体)。该隐式定义的析构函数具有空函数体。

若此满足 constexpr 析构函数 (C++23 前) constexpr 函数 (C++23 起) 的要求,则生成的析构函数为 constexpr

(C++20 起)


被删除的析构函数

当满足以下任意条件时,类 T 的隐式声明或显式默认的析构函数会被定义为已删除:

  • T 拥有类类型 M (或其可能的多维数组)的 潜在构造子对象 ,且 M 的析构函数满足以下任一条件:
  • 已被删除或无法从 T 的析构函数访问,或
  • 当该子对象为 变体成员 时,该析构函数为非平凡析构函数
(C++26 前)
  • T 非联合体类型,且拥有类类型 M (或其可能的多维数组)的非 变体 潜在构造子对象 ,且 M 的析构函数已被删除或无法从 T 的析构函数访问
  • T 为联合体类型,且满足以下任一条件:
  • 为重载决议选择用于默认初始化 T 类型对象的构造函数时,决议失败或选择了已被删除或非平凡的构造函数
  • T 拥有类类型 M (或其可能的多维数组)的变体成员 V ,其中 V 具有默认初始化器且 M 具有非平凡析构函数
(C++26 起)
  • 存在歧义,或
  • 找到的函数已被删除或无法从析构函数访问

若显式默认的预期析构函数不是 T 的析构函数,则其被定义为已删除

(C++20 起)
(C++11 起)

平凡析构函数

当满足以下所有条件时,类 T 的析构函数是平凡的:

  • 析构函数是 隐式声明的 (C++11 前) 用户提供 (C++11 起)
  • 析构函数不是虚函数。
  • 所有直接基类都具有平凡析构函数。
  • 每个类类型(或类类型数组)的非静态数据成员都具有平凡析构函数。
(直至 C++26)
  • T 要么是联合体,要么每个非变体非静态数据成员(类类型或类类型数组)都具有平凡析构函数。
(自 C++26 起)

平凡析构函数是不执行任何操作的析构函数。具有平凡析构函数的对象不需要 delete 表达式,仅通过释放其存储空间即可完成销毁。所有与C语言兼容的数据类型(POD类型)都具有平凡可析构性。

析构序列

对于用户定义或隐式定义的析构函数,在执行完析构函数体并销毁函数体内分配的所有自动对象后,编译器会按声明顺序的逆序调用该类的所有非静态非变体数据成员的析构函数,随后按 构造顺序的逆序 调用所有直接非虚基类的析构函数(这些基类析构函数会继续调用其成员和基类的析构函数,依此类推),最后若该对象属于 最终派生类 ,则会调用所有虚基类的析构函数。

即使直接调用析构函数(例如 obj.~Foo ( ) ; ), return 语句在 ~Foo ( ) 中也不会立即将控制权返回给调用者:它会先调用所有成员和基类的析构函数。

虚析构函数

通过基类指针删除对象会引发未定义行为,除非基类中的析构函数是 virtual

class Base
{
public:
    virtual ~Base() {}
};
class Derived : public Base {};
Base* b = new Derived;
delete b; // 安全

一个常见的指导原则是,基类的析构函数必须 要么是公开且虚函数,要么是受保护且非虚函数

纯虚析构函数

一个 预期 (C++20 起) 析构函数可以声明为 纯虚函数 ,例如在需要设为抽象类但不存在其他适合声明为纯虚函数的基类中。纯虚析构函数必须具有定义,因为当派生类被销毁时,所有基类析构函数总是会被调用:

class AbstractBase
{
public:
    virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}
class Derived : public AbstractBase {};
// AbstractBase obj; // 编译器错误
Derived obj;         // 正确

异常

与其他函数一样,析构函数可能通过抛出 异常 来终止 (这通常需要显式声明为 noexcept ( false ) (C++11 起) ,但若该析构函数恰好在 栈展开 过程中被调用,则会改为调用 std::terminate

尽管 std::uncaught_exceptions 有时可用于检测正在进行的栈展开,但允许任何析构函数通过抛出异常来终止通常被视为不良实践。不过某些库仍会使用此功能,例如 SOCI Galera 3 ,这些库依赖于无名临时对象的析构函数在构造该临时对象的完整表达式结束时抛出异常的能力。

std::experimental::scope_success 在 Library fundamental TS v3 中可能具有 潜在抛出异常的析构函数 ,当作用域正常退出且退出函数抛出异常时,该析构函数会抛出异常。

注释

对普通对象(如局部变量)直接调用析构函数,会在作用域结束时再次调用析构函数,这将引发未定义行为。

在泛型语境中,析构函数调用语法可用于非类类型的对象;这被称为伪析构函数调用:参见 成员访问运算符

特性测试宏 标准 特性
__cpp_trivial_union 202502L (C++26) 放宽联合体特殊成员函数的平凡性要求

示例

#include <iostream>
struct A
{
    int i;
    A(int num) : i(num)
    {
        std::cout << "ctor a" << i << '\n';
    }
    (~A)() // 但通常写作 ~A()
    {
        std::cout << "dtor a" << i << '\n';
    }
};
A a0(0);
int main()
{
    A a1(1);
    A* p;
    { // 嵌套作用域
        A a2(2);
        p = new A(3);
    } // a2 离开作用域
    delete p; // 调用 a3 的析构函数
}

输出:

ctor a0
ctor a1
ctor a2
ctor a3
dtor a2
dtor a3
dtor a1
dtor a0

缺陷报告

以下行为变更缺陷报告被追溯应用于先前发布的C++标准。

缺陷报告 适用标准 发布时行为 正确行为
CWG 193 C++98 未指定析构函数中的自动对象是在类的基类与成员子对象销毁之前还是之后被销毁 应在销毁这些子对象之前销毁自动对象
CWG 344 C++98 析构函数的声明符语法存在缺陷(与 CWG 194 CWG 263 问题相同) 将语法改为专用函数声明符语法
CWG 1241 C++98 静态成员可能在析构函数执行后立即被销毁 仅销毁非静态成员
CWG 1353 C++98 隐式声明析构函数未定义的条件未考虑多维数组类型 需考虑这些类型
CWG 1435 C++98 析构函数声明符语法中“类名”的含义不明确 将语法改为专用函数声明符语法
CWG 2180 C++98 非最终派生类的析构函数会调用其虚直接基类的析构函数 不会调用这些析构函数
CWG 2807 C++20 声明说明符可能包含 consteval 已禁止该用法

参见