析构函数
析构函数是一种特殊的 成员函数,它在 对象的生命周期 结束时被调用。析构函数的目的是释放对象在其生命周期内可能已获取的资源。
析构函数不能是 协程。 |
(自 C++20 起) |
内容 |
[编辑] 语法
析构函数(直到 C++20)预期析构函数(自 C++20 起) 使用以下形式的成员 函数声明符 声明
带有波浪号的类名 ( 参数列表 (可选) ) 异常 (可选) 属性 (可选) |
|||||||||
带有波浪号的类名 | - | 一个 标识符表达式,可能后跟 属性 列表,并且(自 C++11 起) 可能用一对括号括起来 | ||||||
参数列表 | - | 参数列表 | ||||||
异常 | - |
| ||||||
属性 | - | (自 C++11 起) 属性 列表 |
在预期(自 C++20 起)析构函数声明的 声明说明符 中允许的唯一说明符是constexpr
,(自 C++11 起) friend
,inline
和 virtual
(特别是,不允许返回类型)。
带有波浪号的类名 的标识符表达式必须具有以下形式之一
- 否则,标识符表达式是一个限定标识符,其终端非限定标识符为 ~ 后跟由限定标识符的非终端部分指定的类的注入类名。
[edit] 解释
析构函数在对象的生命周期结束时被隐式调用,包括
|
(自 C++11 起) |
- 作用域结束,对于具有自动存储时长的对象,以及生命周期通过绑定到引用而延长了的临时对象
- delete 表达式,对于具有动态存储时长的对象
- 完整 表达式 的结束,对于无名临时对象
- 栈展开,对于具有自动存储时长的对象,当异常未捕获地逃逸出它们的块时。
析构函数也可以显式调用。
[edit] 可能被调用的析构函数
在以下情况下,类 C
的析构函数会被可能调用
- 它被显式或隐式调用。
- new 表达式 创建了一个类型为
C
的对象数组。 - return 语句 的结果对象类型为
C
。 - 数组正在进行 聚合初始化,并且其元素类型为
C
。 - 类对象正在进行聚合初始化,并且它有一个类型为
C
的成员,其中C
不是 匿名联合 类型。 - 在非 委托(自 C++11 起) 构造函数中,一个 可能被构造的子对象 类型为
C
。 - 构造了一个类型为
C
的 异常对象。
如果一个可能被调用的析构函数被删除或(自 C++11 起)无法从调用上下文访问,则程序格式错误。
预期析构函数一个类可以有一个或多个预期析构函数,其中一个会被选中作为该类的析构函数。 为了确定哪个预期析构函数是析构函数,在类定义结束时,对在类中声明的具有空参数列表的预期析构函数进行 重载解析。如果重载解析失败,则程序格式错误。析构函数选择不会 odr-使用 所选的析构函数,并且所选的析构函数可能是被删除的。 所有预期析构函数都是特殊成员函数。如果类 |
(自 C++20 起) |
[edit] 隐式声明的析构函数
如果 类类型 没有提供用户声明的预期(自 C++20 起)析构函数,编译器将始终声明一个析构函数作为其类的 inline public 成员。
与任何隐式声明的特殊成员函数一样,隐式声明的析构函数的异常说明是非抛出型,除非 任何可能被构造的基类或成员的析构函数是 可能抛出型(自 C++17 起)隐式定义将直接调用具有不同异常说明的函数(直到 C++17)。在实践中,隐式析构函数是 noexcept,除非类被基类或成员的析构函数是 noexcept(false) "污染"。
[edit] 隐式定义的析构函数
如果隐式声明的析构函数没有被删除,那么它在被 odr-使用 时会被编译器隐式定义(即,生成并编译函数体)。这个隐式定义的析构函数有一个空函数体。
如果这满足了 constexpr 析构函数(直到 C++23)constexpr 函数(自 C++23 起) 的要求,则生成的析构函数是 constexpr。 |
(自 C++20 起) |
[edit] 被删除的析构函数
如果满足以下任一条件,则类 T
的隐式声明或显式默认的析构函数会被定义为未定义(直到 C++11)定义为删除(自 C++11 起)
-
T
具有一个类类型M
(或可能是其多维数组)的 可能被构造的子对象,使得M
具有一个析构函数,该析构函数
- 被删除或无法从
T
的析构函数访问,或者 - 在子对象是 变体成员 的情况下,是非平凡的。
- 被删除或无法从
- 析构函数是虚函数,并且对 释放函数 的查找结果是
- 歧义,或者
- 一个从析构函数被删除或无法访问的函数。
如果一个显式默认的预期析构函数 |
(自 C++20 起) |
[edit] 平凡析构函数
如果满足以下所有条件,则类 T
的析构函数是平凡的
- 析构函数不是用户提供的(即,它是隐式声明的,或者在首次声明时被显式定义为默认)。
- 析构函数不是虚函数(即,基类析构函数不是虚函数)。
- 所有直接基类都具有平凡析构函数。
- 所有非静态数据成员的类类型(或类类型数组)都具有平凡析构函数。
平凡析构函数是不执行任何操作的析构函数。具有平凡析构函数的对象不需要 delete 表达式,可以通过简单地释放其存储空间来销毁。所有与 C 语言兼容的数据类型(POD 类型)都是平凡可析构的。
[edit] 析构顺序
对于用户定义或隐式定义的析构函数,在执行析构函数体并销毁在函数体内分配的任何自动对象之后,编译器会按声明顺序的逆序调用类中所有非静态非变体数据成员的析构函数,然后它会按 构造顺序的逆序 调用所有直接非虚基类的析构函数(依次调用其成员和基类的析构函数,等等),然后,如果该对象是最派生类,则它会调用所有虚基类的析构函数。
即使析构函数被直接调用(例如 obj.~Foo();),~Foo() 中的 return 语句不会立即将控制权返回给调用者:它会首先调用所有这些成员和基类析构函数。
[edit] 虚析构函数
通过指向基类的指针删除对象会导致未定义的行为,除非基类中的析构函数是 虚函数
class Base { public: virtual ~Base() {} }; class Derived : public Base {}; Base* b = new Derived; delete b; // safe
一个常见的准则是,基类的析构函数必须是 公有且虚函数或受保护且非虚函数。
[edit] 纯虚析构函数
一个预期(自 C++20 起)析构函数可以被声明为 纯虚函数,例如在需要被设置为抽象类的基类中,但没有其他合适的函数可以被声明为纯虚函数。纯虚析构函数必须有一个定义,因为所有基类析构函数在派生类被销毁时都会被调用。
class AbstractBase { public: virtual ~AbstractBase() = 0; }; AbstractBase::~AbstractBase() {} class Derived : public AbstractBase {}; // AbstractBase obj; // compiler error Derived obj; // OK
[edit] 异常
与任何其他函数一样,析构函数可以通过抛出 异常 来终止 (这通常需要它被显式声明为 noexcept(false))(自 C++11 起),但是如果该析构函数恰好在 栈展开 期间被调用,则 std::terminate 会被调用。
虽然 std::uncaught_exceptions 有时可用于检测正在进行的栈展开,但通常认为允许任何析构函数通过抛出异常来终止是一种不好的做法。但是,某些库(例如 SOCI 和 Galera 3)利用了此功能,它们依赖于无名临时对象的析构函数能够在构造临时对象的完整表达式的末尾抛出异常。
库基础 TS v3 中的 std::experimental::scope_success 可能具有 可能抛出异常的析构函数,该函数在作用域正常退出并且退出函数抛出异常时会抛出异常。
[编辑] 注释
直接调用普通对象的析构函数(例如局部变量)会导致未定义的行为,当在作用域结束时再次调用析构函数时。
在泛型上下文中,析构函数调用语法可以用于非类类型的对象;这被称为伪析构函数调用:参见 成员访问运算符。
[编辑] 示例
输出
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 问题 194 和 CWG 问题 263 存在相同问题) 将语法更改为专门的 |
函数声明语法 |
CWG 1241 | C++98 | 静态成员可能在 析构函数执行后立即被销毁 |
只销毁非 静态成员 |
C++98 | CWG 1353 隐式声明的析构函数未定义的条件没有考虑多维数组类型 |
考虑这些类型 | |
C++98 | CWG 1435 析构函数声明语法中“类名”的含义尚不清楚 |
函数声明语法 | |
C++98 | CWG 2180 类 X 的析构函数调用了 |
X 的虚拟直接基类的析构函数 | |
那些析构函数不会被调用 | CWG 2807 | C++20 |