命名空间
变体
操作

析构函数

来自 cppreference.cn
< cpp‎ | 语言
 
 
C++ 语言
通用主题
流程控制
条件执行语句
if
迭代语句(循环)
跳转语句
函数
函数声明
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 起)使用以下形式的成员函数声明符声明

带波浪号的类名 ( 形参列表 (可选) ) 异常说明 (可选) 属性 (可选)
带波浪号的类名 - 一个标识符表达式后面可以跟随一个属性列表,并且(C++11 起)可以被一对括号括起来
形参列表 - 形参列表 (必须为空或 void)
异常规范 -

动态异常规范

(C++11 前)

或者动态异常规范
或者noexcept 规范

(C++11 起)
(C++17 前)

noexcept 规范的一部分

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

一个(C++20 起)析构函数声明的声明说明符中允许的说明符只有constexpr(C++11 起) friendinlinevirtual (特别地,不允许有返回类型)。

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

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

[编辑] 解释

每当对象的生存期结束时,就会隐式调用析构函数,这包括:

  • 对于具有线程局部存储期的对象,在线程退出时
(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 的析构函数在以下情况下是可能被调用的

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

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

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

与任何隐式声明的特殊成员函数一样,隐式声明的析构函数的异常说明是非抛出的,除非任何可能被构造的基类或成员的析构函数是可能抛出的(C++17 起)隐式定义会直接调用一个具有不同异常说明的函数(C++17 前)。实际上,隐式析构函数是 noexcept 的,除非该类被一个基类或成员“毒化”,而该基类或成员的析构函数是 noexcept(false)

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

如果一个隐式声明的析构函数没有被删除,那么当它被 ODR 式使用时,编译器会隐式定义它(即,生成并编译一个函数体)。这个隐式定义的析构函数有一个空的函数体。

如果这满足了 constexpr 析构函数(C++23 前) constexpr 函数(C++23 起)的要求,那么生成的析构函数是 constexpr 的。

(C++20 起)


被删除的析构函数

T 的隐式声明或显式默认的析构函数被定义为已删除的,如果满足以下任何一个条件:

  • 被删除或从 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();),~Foo() 中的 return 语句也不会立即将控制权返回给调用者:它会先调用所有那些成员和基类的析构函数。

[编辑] 虚析构函数

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

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

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

[编辑] 纯虚析构函数

一个(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 可能有一个可能抛出的析构函数,它在作用域正常退出且退出函数抛出异常时抛出异常。

[编辑] 注意

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

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

功能测试宏 标准 特性
__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++ 标准。

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

[编辑] 参阅