名稱空間
變體
操作

解構函式

來自 cppreference.com
< 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 已禁止

[編輯] 參閱