命名空间
变体
操作

析构函数

来自 cppreference.cn
< cpp‎ | language
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
for
范围 for (C++11)
跳转语句
函数
函数声明
Lambda 函数表达式
inline 说明符
动态异常规范 (直到 C++17*)
noexcept 说明符 (C++11)
异常
命名空间
类型
说明符
const/volatile
decltype (C++11)
auto (C++11)
constexpr (C++11)
consteval (C++20)
constinit (C++20)
存储期说明符
初始化
 
 

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

析构函数不能是协程

(自 C++20 起)

目录

[编辑] 语法

析构函数(直到 C++20)前瞻析构函数(自 C++20 起) 使用以下形式的成员函数声明符声明

带波浪号的类名 ( 参数列表 (可选) ) except (可选) attr (可选)
带波浪号的类名 - 标识符表达式可能后跟属性列表,以及(自 C++11 起)可能用一对括号括起来
参数列表 - 参数列表(必须为空或 void
except -

动态异常规范

(直到 C++11)

要么是动态异常规范
要么是noexcept 规范

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

noexcept 规范

(自 C++17 起)
attr - (自 C++11 起) 属性列表

声明说明符中,前瞻(自 C++20 起)析构函数声明中唯一允许的说明符是constexpr(自 C++11 起) friendinlinevirtual(特别是,不允许返回类型)。

带波浪号的类名的标识符表达式必须具有以下形式之一

  • 对于类,标识符表达式是 ~ 后跟直接封闭类的注入类名
  • 对于类模板,标识符表达式是 ~ 后跟命名当前实例化的类名(直到 C++20)直接封闭类模板的注入类名(自 C++20 起)
  • 否则,标识符表达式是限定标识符,其终端非限定标识符是 ~ 后跟由限定标识符的非终端部分指定的类的注入类名。

[编辑] 解释

每当对象的生命周期结束时,析构函数都会被隐式调用,包括

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

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

前瞻析构函数

一个类可以有一个或多个前瞻析构函数,其中一个被选为该类的析构函数。

为了确定哪个前瞻析构函数是析构函数,在该类定义结束时,在类中声明的带有空参数列表的前瞻析构函数之间执行重载解析。 如果重载解析失败,则程序是非良构的。 析构函数选择不odr-use选定的析构函数,并且选定的析构函数可能会被删除。

所有前瞻析构函数都是特殊的成员函数。 如果没有为类 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 的析构函数在以下情况下可能被调用

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

[编辑] 隐式声明的析构函数

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

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

[编辑] 隐式定义的析构函数

如果隐式声明的析构函数未被删除,则在odr-used时,编译器会隐式定义它(即,生成并编译函数体)。 此隐式定义的析构函数具有空函数体。

如果这满足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 的前瞻析构函数不是 T 的析构函数,则将其定义为已删除。

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

[编辑] 平凡析构函数

如果满足以下所有条件,则类 T 的析构函数是平凡的

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

平凡析构函数是不执行任何操作的析构函数。 具有平凡析构函数的对象不需要 delete 表达式,并且可以通过简单地释放其存储空间来处置。 与 C 语言兼容的所有数据类型(POD 类型)都是可平凡析构的。

[编辑] 析构序列

对于用户定义或隐式定义的析构函数,在执行析构函数的主体并销毁在主体内分配的任何自动对象之后,编译器会以声明的相反顺序调用该类的所有非静态非变体数据成员的析构函数,然后它会按照构造的相反顺序调用所有直接非虚基类的析构函数(这反过来又会调用其成员及其基类的析构函数,等等),然后,如果此对象是最派生类,则它会调用所有虚基类的析构函数。

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

[编辑] 虚析构函数

通过指向基类的指针删除对象会调用未定义的行为,除非基类中的析构函数是虚函数

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

一个常见的指导原则是,基类的析构函数必须是public 和 virtual 或 protected 和 nonvirtual

[编辑] 纯虚析构函数

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

class AbstractBase
{
public:
    virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}
 
class Derived : public AbstractBase {};
 
// AbstractBase obj; // compiler error
Derived obj;         // OK

[编辑] 异常

与任何其他函数一样,析构函数可能会通过抛出异常来终止(这通常需要将其显式声明为 noexcept(false)(自 C++11 起),但是,如果此析构函数恰好在栈展开期间被调用,则会调用 std::terminate 来代替。

尽管 std::uncaught_exceptions 有时可用于检测正在进行的栈展开,但通常认为允许任何析构函数通过抛出异常来终止是不良做法。 然而,一些库(例如 SOCIGalera 3)使用了此功能,这些库依赖于无名临时对象的析构函数在构造临时对象的完整表达式末尾抛出异常的能力。

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

[编辑] 注解

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

在泛型上下文中,析构函数调用语法可以与非类类型的对象一起使用;这被称为伪析构函数调用:请参阅成员访问运算符

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

[编辑] 示例

#include <iostream>
 
struct A
{
    int i;
 
    A(int num) : i(num)
    {
        std::cout << "ctor a" << i << '\n';
    }
 
    (~A)() // but usually ~A()
    {
        std::cout << "dtor a" << i << '\n';
    }
};
 
A a0(0);
 
int main()
{
    A a1(1);
    A* p;
 
    { // nested scope
        A a2(2);
        p = new A(3);
    } // a2 out of scope
 
    delete p; // calls the destructor of a3
}

输出

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

[编辑] 缺陷报告

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

DR 应用于 已发布行为 正确行为
CWG 193 C++98 析构函数中的自动对象是否在
类基类和成员子对象的析构之前或之后销毁是不确定的
类基类和成员子对象的析构之前或之后销毁是不确定的
它们被销毁
在销毁之前
这些子对象
CWG 344 C++98 析构函数的声明符语法有缺陷(与 CWG issue 194CWG issue 263 具有相同的问题
析构函数的声明符语法有缺陷(与 CWG issue 194CWG issue 263 具有相同的问题
将语法更改为专门的
函数声明符语法
CWG 1241 C++98 静态成员可能会被销毁
在析构函数执行之后立即
仅销毁非
静态成员
CWG 1353 C++98 隐式声明的析构函数未定义的条件
没有考虑多维数组类型
考虑这些类型
CWG 1435 C++98 “类名”在
析构函数的声明符语法中的含义不明确
将语法更改为专门的
函数声明符语法
CWG 2180 C++98 不是最派生类的类的析构函数
将调用其虚拟直接基类的析构函数
它不会调用这些析构函数
CWG 2807 C++20 声明说明符可以包含 consteval 禁止

[编辑] 参见