解構函式 (C++)
解構函式是成員函式,當物件超出範圍或由 或delete[]
呼叫delete
明確終結時,會自動叫用。 解構函式的名稱與 類別相同,前面有一個Tilde (~
)。 例如,String
類別的解構函式宣告為:~String()
。
如果您未定義解構函式,編譯程式會提供預設解構函式,而且對於某些類別而言,這已足夠。 當 類別維護必須明確釋放的資源時,您必須定義自定義解構函式,例如系統資源的句柄,或當類別實例終結時應該釋放的記憶體指標。
請考慮下面的 String
類別宣告:
// spec1_destructors.cpp
#include <string> // strlen()
class String
{
public:
String(const char* ch); // Declare the constructor
~String(); // Declare the destructor
private:
char* _text{nullptr};
};
// Define the constructor
String::String(const char* ch)
{
size_t sizeOfText = strlen(ch) + 1; // +1 to account for trailing NULL
// Dynamically allocate the correct amount of memory.
_text = new char[sizeOfText];
// If the allocation succeeds, copy the initialization string.
if (_text)
{
strcpy_s(_text, sizeOfText, ch);
}
}
// Define the destructor.
String::~String()
{
// Deallocate the memory that was previously reserved for the string.
delete[] _text;
}
int main()
{
String str("We love C++");
}
在上述範例中,解構函式 String::~String
會使用 delete[]
運算符來解除配置動態配置給文字儲存的空間。
宣告解構函式
解構函式是名稱與類別相同的函式,但其名稱前面會加上波狀符號 (~
)。
有數種規則用於管理解構函式的宣告。 解構函式:
- 不接受自變數。
- 不要傳回值 (或
void
)。 - 無法宣告為
const
、volatile
或static
。 不過,可以叫用它們來解構宣告為const
、volatile
或static
的物件。 - 可以宣告為
virtual
。 使用虛擬解構函式,您可以在不知道物件類型的情況下終結物件,而使用虛擬函式機制叫用對象的正確解構函式。 解構函式也可以宣告為抽象類的純虛擬函式。
使用解構函式
在下列任一事件發生時,會呼叫解構函式:
- 區塊範圍內的區域 (自動) 物件會超出範圍。
- 使用
delete
來解除分配使用new
配置的物件。 使用delete[]
會導致未定義的行為。 - 使用
delete[]
來解除分配使用new[]
配置的物件。 使用delete
會導致未定義的行為。 - 暫存物件的存留期結束。
- 程式結束,而全域或靜態物件存在。
- 使用解構函式的函式完整名稱明確地呼叫解構函式。
解構函式可以自由呼叫類別成員函式和存取類別成員資料。
使用解構函式有兩項限制:
您無法取得其位址。
衍生類別不會繼承其基類的解構函式。
解構順序
當物件超出範圍或被刪除時,事件完整解構的順序如下所示:
呼叫類別的解構函式,並且執行解構函式的主體。
按照非靜態成員物件出現在類別解構函式中的順序反向呼叫其解構函式。 用於建構這些成員的選擇性成員初始化清單不會影響建構或解構的順序。
非虛擬基類的解構函式會以宣告的反向順序呼叫。
按照宣告的相反順序呼叫虛擬基底類別的解構函式。
// order_of_destruction.cpp
#include <cstdio>
struct A1 { virtual ~A1() { printf("A1 dtor\n"); } };
struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };
struct B1 { ~B1() { printf("B1 dtor\n"); } };
struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };
int main() {
A1 * a = new A3;
delete a;
printf("\n");
B1 * b = new B3;
delete b;
printf("\n");
B3 * b2 = new B3;
delete b2;
}
A3 dtor
A2 dtor
A1 dtor
B1 dtor
B3 dtor
B2 dtor
B1 dtor
虛擬基底類別
虛擬基底類別的解構函式會依它們出現在導向非循環圖中的反向順序呼叫 (深度優先、由左至右、後序走訪)。 下圖將說明繼承圖表。
標示為 A 到 E 的五個類別會排列在繼承圖形中。 類別 E 是 B、C 和 D 的基類。C 和 D 類別是 A 和 B 的基類。
下列欄出圖中所顯示類別的類別定義:
class A {};
class B {};
class C : virtual public A, virtual public B {};
class D : virtual public A, virtual public B {};
class E : public C, public D, virtual public B {};
為了判斷 E
類型物件之虛擬基底類別的解構順序,編譯器會套用下列演算法來建置清單:
- 周遊左側圖表,從圖中的最深點開始 (在這個案例中為
E
)。 - 向左周遊,直到瀏覽過所有節點。 記下目前節點的名稱。
- 再次瀏覽上一個節點 (右下方),確認記住的節點是否為虛擬基底類別。
- 如果記住的節點是虛擬基底類別,請掃描清單,查看該節點是否已輸入。 如果不是虛擬基類,請忽略它。
- 如果記住的節點尚未出現在清單中,請將它新增至清單底部。
- 向上並沿著下一個向右的路徑周遊圖表。
- 移至步驟 2。
- 到達最後一個向上路徑時,請記下目前節點的名稱。
- 移至步驟 3。
- 繼續這個程序,直到底部節點再次成為目前節點為止。
因此,E
類別的解構順序如下:
- 非虛擬基類
E
。 - 非虛擬基類
D
。 - 非虛擬基類
C
。 - 虛擬基底類別
B
。 - 虛擬基底類別
A
。
這個程序會產生已排序的唯一項目清單。 類別名稱不會重複出現。 建構清單之後,它會以反向順序進行逐步解構,而清單中每個類別的解構函式會從最後一個類別呼叫到第一個類別。
建構或解構的順序主要是當某個類別中的建構函式或解構函式依賴第一個建立的另一個元件或保存較長的時間時,主要很重要,例如,如果解構函式 (如先前所示的圖所示) B
的解構函A
式在執行程式代碼時仍存在,反之亦然。
繼承圖表中類別之間的這種相依性原本就存在危險性,因為之後衍生的類別可以修改最左邊的路徑,藉此變更建構和解構的順序。
非虛擬基類
非虛擬基類的解構函式會以宣告基類名稱的反向順序呼叫。 請考慮下列類別宣告:
class MultInherit : public Base1, public Base2
...
在上述範例中,Base2
的解構函式是在 Base1
的解構函式之前呼叫。
明確解構函式呼叫
明確呼叫解構函式不是必要的步驟。 不過,這對放置於絕對位址的物件執行清除作業可能會很有用。 這些物件通常會使用採用 placement 自變數的使用者定義 new
運算符來配置。 運算子 delete
無法解除分配此記憶體,因為它不是從免費存放區配置(如需詳細資訊,請參閱 新的和刪除運算符)。 不過,呼叫解構函式時,可以執行適當的清除作業。 若要明確呼叫物件的解構函式 (s
類別的 String
),請使用下列其中一種陳述式:
s.String::~String(); // non-virtual call
ps->String::~String(); // non-virtual call
s.~String(); // Virtual call
ps->~String(); // Virtual call
您可以使用明確呼叫解構函式的標註法 (如先前所示),不論該類型是否定義了解構函式。 這可讓您進行這類明確呼叫,而不需要知道是否已為該類型定義解構函式。 明確呼叫未定義的解構函式不會有任何作用。
穩固程式設計
如果類別取得資源,而且必須安全地管理資源,則類別需要解構函式,而它可能必須實作複製建構函式和複製指派。
如果使用者未定義這些特殊函式,編譯程式會隱含定義這些特殊函式。 隱含產生的建構函式和指派運算符會執行淺層、成員式複製,如果物件正在管理資源,這幾乎是錯誤的。
在下一個範例中,隱含產生的複製建構函式會建立指標 str1.text
並 str2.text
參考相同的記憶體,而當我們從 copy_strings()
傳回 時,該記憶體將會刪除兩次,這是未定義的行為:
void copy_strings()
{
String str1("I have a sense of impending disaster...");
String str2 = str1; // str1.text and str2.text now refer to the same object
} // delete[] _text; deallocates the same memory twice
// undefined behavior
明確定義解構函式、複製建構函式或複製指派運算符,可防止移動建構函式和移動指派運算元的隱含定義。 在此情況下,如果複製成本昂貴,通常無法提供移動作業,則遺漏優化機會。