析构函数
析构函数是一种特殊的成员函数,它在对象的生存期结束时被调用。析构函数的目的是释放对象在其生存期内可能获取的资源。
析构函数不能是协程。 |
(C++20 起) |
目录 |
[编辑] 语法
析构函数(C++20 前)准析构函数(C++20 起)使用以下形式的成员函数声明符声明
带波浪号的类名 ( 形参列表 (可选) ) 异常说明 (可选) 属性 (可选) |
|||||||||
带波浪号的类名 | - | 一个标识符表达式,后面可以跟随一个属性列表,并且(C++11 起)可以被一对括号括起来 | ||||||
形参列表 | - | 形参列表 (必须为空或 void) | ||||||
异常规范 | - |
| ||||||
属性 | - | (C++11 起) 属性列表 |
一个准(C++20 起)析构函数声明的声明说明符中允许的说明符只有constexpr
、(C++11 起) friend
、inline
和 virtual
(特别地,不允许有返回类型)。
带波浪号的类名的标识符表达式必须具有以下形式之一
- 否则,标识符表达式是一个限定标识符,其末端的非限定标识符为 ~ 后跟由该限定标识符的非末端部分所指名的类的注入类名。
[编辑] 解释
每当对象的生存期结束时,就会隐式调用析构函数,这包括:
|
(C++11 起) |
- 对于具有自动存储期的对象以及其生命周期因绑定到引用而被延长的临时对象,在作用域结束时
- 对于具有动态存储期的对象,在使用 delete 表达式时
- 对于无名临时对象,在完整表达式结束时
- 当异常逃离其块且未被捕获时,对于具有自动存储期的对象,在栈回溯期间
析构函数也可以被显式调用。
准析构函数一个类可以有一个或多个准析构函数,其中一个被选为该类的析构函数。 为了确定哪个准析构函数是析构函数,在类定义结束时,会对类中声明的具有空参数列表的准析构函数执行重载决议。如果重载决议失败,则程序非良构。析构函数的选择不会ODR 式使用所选的析构函数,且所选的析构函数可以是被删除的。 所有准析构函数都是特殊成员函数。如果类 运行此代码 #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
的析构函数在以下情况下是可能被调用的
- 它被显式或隐式调用。
- 一个 new 表达式创建了类型为
T
的对象数组。 - return 语句的结果对象类型为
T
。 - 一个数组正在进行聚合初始化,并且其元素类型是
T
。 - 一个类对象正在进行聚合初始化,并且它有一个类型为
T
的成员,其中T
不是匿名联合体类型。 - 一个可能被构造的子对象在非委托(C++11 起)构造函数中类型为
T
。 - 构造了一个类型为
T
的异常对象。
如果一个可能被调用的析构函数是被删除的或(C++11 起)从调用上下文中不可访问的,则程序非良构。
[编辑] 隐式声明的析构函数
如果一个类类型没有提供用户声明的准(C++20 起)析构函数,编译器将总是声明一个析构函数作为其类的 inline public 成员。
与任何隐式声明的特殊成员函数一样,隐式声明的析构函数的异常说明是非抛出的,除非任何可能被构造的基类或成员的析构函数是可能抛出的(C++17 起)隐式定义会直接调用一个具有不同异常说明的函数(C++17 前)。实际上,隐式析构函数是 noexcept 的,除非该类被一个基类或成员“毒化”,而该基类或成员的析构函数是 noexcept(false)。
[编辑] 隐式定义的析构函数
如果一个隐式声明的析构函数没有被删除,那么当它被 ODR 式使用时,编译器会隐式定义它(即,生成并编译一个函数体)。这个隐式定义的析构函数有一个空的函数体。
如果这满足了 constexpr 析构函数(C++23 前) constexpr 函数(C++23 起)的要求,那么生成的析构函数是 constexpr 的。 |
(C++20 起) |
被删除的析构函数类
|
(C++11 起) |
[编辑] 平凡析构函数
类 T
的析构函数是平凡的,如果满足以下所有条件:
- 析构函数是隐式声明的(C++11 前)非用户提供的(C++11 起)。
- 析构函数不是虚函数。
- 所有直接基类都有平凡析构函数。
|
(直到 C++26) |
|
(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 有时可用于检测正在进行的栈回溯,但通常认为允许任何析构函数通过抛出异常来终止是一种不好的做法。尽管如此,一些库还是使用了这个功能,例如 SOCI 和 Galera 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 问题 194 和 CWG 问题 263 有相同的问题) |
将语法更改为专门的 函数声明符语法 |
CWG 1241 | C++98 | 静态成员可能在析构函数 执行后立即被销毁 |
只销毁非 静态成员 |
CWG 1353 | C++98 | 隐式声明的析构函数被定义为已删除的 条件没有考虑多维数组类型 |
考虑这些类型 |
CWG 1435 | C++98 | “类名”在析构函数的 声明符语法中的含义不明确 |
将语法更改为专门的 函数声明符语法 |
CWG 2180 | C++98 | 非最终派生类的析构函数 会调用其虚直接基类的析构函数 |
它将不会调用那些析构函数 |
CWG 2807 | C++20 | 声明说明符可以包含 consteval | 已禁止 |