命名空間
變體
動作

解構子

出自 cppreference.com
< 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)
儲存期指定符
初始化
 
 

解構子是一種特殊的成員函式,會在物件生命週期結束時被呼叫。解構子的目的是釋放物件在其生命週期內可能已獲取的資源。

解構子不能是協程 (coroutine)

(自 C++20 起)

目錄

[編輯] 語法

解構子(C++20 前)預期解構子(C++20 起) 是使用以下形式的成員函式宣告子來宣告:

class-name-with-tilde ( parameter-list (可選) ) except (可選) attr (可選)
class-name-with-tilde - 一個識別字表達式後面可能接有屬性列表,且(C++11 起)可能由一對括號包圍
參數列表 - 參數列表(必須為空或是 void
except -

動態異常規格

(直到 C++11)

要麼是動態異常規格
要麼是noexcept 規格

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

noexcept 規格

(自 C++17 起)
屬性 - (C++11 起) 一個屬性列表。

預期(C++20 起)解構子宣告的宣告說明符中,唯一允許的說明符是constexpr(C++11 起) friendinlinevirtual(特別注意,不允許指定回傳型別)。

class-name-with-tilde 的識別字表達式必須具備以下其中一種形式:

  • 否則,該識別字表達式為限定識別字 (qualified identifier),其結尾的未限定識別字為 ~ 後接由限定識別字非終端部分所指定的類別的注入類別名稱。

[編輯] 說明

當物件的生命週期結束時,解構子會被隱式呼叫,這包含:

  • 執行緒結束時,針對具有執行緒區域儲存期的物件。
(C++11 起)
  • 作用域結束時,針對具有自動儲存期的物件,以及其生命週期因綁定到參考而被延長的暫存物件。
  • delete 表達式,針對具有動態儲存期的物件。
  • 完整表達式結束時,針對無名的暫存物件。
  • 堆疊展開 (stack unwinding),當例外未被捕捉並逃離其區塊時,針對具有自動儲存期的物件。

解構子也可以被顯式呼叫。

預期解構子 (Prospective destructor)

一個類別可以有一個或多個預期解構子,其中一個會被選為該類別的解構子。

為了確定哪個預期解構子是真正的解構子,在類別定義結束時,會針對該類別中參數列表為空的預期解構子進行重載決議 (overload resolution)。如果重載決議失敗,則程式格式錯誤。解構子的選擇不會對被選中的解構子進行 ODR 使用 (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 成員解構子。

與任何隱式宣告的特殊成員函式一樣,隱式宣告解構子的例外規格是不拋出例外的,除非任何潛在構造的基底類別或成員的解構子是潛在拋出異常 (potentially-throwing)(C++17 起)隱式定義會直接呼叫具有不同例外規格的函式(C++17 前)。在實務上,除非類別被其解構子為 noexcept(false) 的基底類別或成員「毒化」,否則隱式解構子皆為 noexcept

[編輯] 隱式定義解構子

如果隱式宣告的解構子未被刪除,當它被 ODR 使用時,編譯器會隱式定義它(即產生並編譯一個函式主體)。這個隱式定義的解構子擁有一個空的函式主體。

如果這滿足 constexpr 解構子(C++23 前)constexpr 函式(C++23 起) 的要求,則生成的解構子為 constexpr

(自 C++20 起)


刪除的解構子

若類別 T 的隱式宣告或顯式預設解構子滿足以下任一條件,則定義為刪除:

  • T 具有類別型別 M(或其可能是多維陣列)的潛在構造子物件,使得 M 的解構子:
  • 是被刪除的,或從 T 的解構子處無法存取,或者
  • 若該子物件為變體成員 (variant member),則該解構子為非平凡的 (non-trivial)。
(直到 C++26)
  • T 不是聯集,且具有非變體潛在構造子物件,其型別為 M(或其多維陣列),使得 M 的解構子被刪除或從 T 的解構子處無法存取。
  • T 是聯集,且滿足以下任一條件:
  • 用於選擇預設初始化 T 型別物件之建構子的重載決議失敗,或是選擇的建構子是被刪除或非平凡的。
  • T 具有型別 M(或其多維陣列)的變體成員 V,其中 V 有預設初始化器,且 M 的解構子是非平凡的。
(C++26 起)
  • 歧義,或
  • 一個從該解構子處無法存取或被刪除的函式。

T 的顯式預設預期解構子不是 T 的解構子,則定義為刪除。

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

[編輯] 平凡解構子

若類別 T 的解構子滿足以下所有條件,則該解構子是平凡的:

  • 該解構子是隱式宣告的(C++11 前)不是由使用者提供的 (user-provided)(C++11 起)
  • 該解構子不是虛擬的。
  • 所有直接基底類別都具有平凡解構子。
  • 所有類別型別(或類別型別陣列)的非靜態資料成員都具有平凡解構子。
(直到 C++26)
  • T 是聯集,或者每個類別型別(或類別型別陣列)的非變體、非靜態資料成員都具有平凡解構子。
(C++26 起)

平凡解構子是不執行任何操作的解構子。具有平凡解構子的物件不需要 delete 表達式,並且可以僅透過釋放其儲存空間來銷毀。所有與 C 語言相容的資料型別(POD 型別)都是可平凡銷毀的。

[編輯] 解構順序

對於使用者定義或隱式定義的解構子,在執行完解構子主體並銷毀在主體內分配的任何自動物件後,編譯器會以宣告的相反順序呼叫類別的所有非靜態非變體資料成員的解構子,接著以建構順序的相反順序呼叫所有直接非虛擬基底類別的解構子(這會進而呼叫其成員與基底類別的解構子,依此類推),最後,如果此物件是最衍生類別 (most derived class) 的物件,它會呼叫所有虛擬基底的解構子。

即使直接呼叫解構子(例如 obj.~Foo();),~Foo() 中的 return 敘述也不會立即將控制權返回給呼叫者:它會先呼叫所有成員與基底類別的解構子。

[編輯] 虛擬解構子

透過基底類別指標刪除物件時,若基底類別中的解構子不是虛擬的,會引發未定義行為。

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

一個常見的準則(Guideline)是:基底類別的解構子必須是公開且虛擬的,或是保護且非虛擬的

[編輯] 純虛擬解構子

一個預期(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,它們依賴於在建構暫存物件的完整表達式末尾,讓無名暫存物件的解構子拋出例外。

Library fundamental TS v3 中的 std::experimental::scope_success 可能擁有一個潛在拋出例外之解構子,當作用域正常結束且離開函式拋出例外時,它會拋出例外。

[編輯] 註解

對於普通物件(如區域變數)直接呼叫解構子,會在作用域結束時再次呼叫解構子時引發未定義行為。

在泛型上下文中,解構子呼叫語法可以用於非類別型別的物件;這稱為偽解構子呼叫 (pseudo-destructor call):參見成員存取運算子

特性測試巨集 數值 標準 功能
__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 問題 194CWG 問題 263 有相同問題)
將語法變更為專用的
函式宣告子語法
CWG 1241 C++98 靜態成員可能在
解構子執行後立即被銷毀
僅銷毀非
靜態成員
CWG 1353 C++98 隱式宣告解構子未定義的條件
沒有考慮多維陣列型別
考量這些型別
CWG 1435 C++98 解構子宣告語法中
“類別名稱”的含義不明確
將語法變更為專用的
函式宣告子語法
CWG 2180 C++98 非最衍生類別的類別之解構子
會呼叫其虛擬直接基底類別的解構子
修正為不會呼叫那些解構子
CWG 2807 C++20 宣告說明符可能包含 consteval 禁止

[編輯] 參閱

English Deutsch 日本語 中文(简体) 中文(繁體)