解構函式是一個成員函式,當物件超出作用域或透過delete
或delete[]
顯式銷毀時,會自動被呼叫。 解構函式的名稱與類別相同,並以波浪符號(~
)為前綴。 例如,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
。
這個程序會產生已排序的唯一項目清單。 類別名稱不會重複出現。 一旦建立清單,就會以反向順序遍歷清單,並從清單中的最後一個類別到第一個類別依次呼叫每個類別的解構函式。
建構或解構的順序至關重要,尤其是當一個類別中的建構函式或解構函式依賴於另一個元件先被建立或持續存在更長時間的情況下。例如,如果A
的解構函式(如前文圖示)需要在執行其程式碼時B
仍然存在,反之亦然。
繼承圖表中類別之間的這種相依性原本就存在危險性,因為之後衍生的類別可以修改最左邊的路徑,藉此變更建構和解構的順序。
非虛擬基類
非虛擬基類的解構函式會以宣告基類名稱的反向順序呼叫。 請考慮下列類別宣告:
class MultInherit : public Base1, public Base2
...
在上述範例中,Base2
的解構函式是在 Base1
的解構函式之前呼叫。
明確解構函式呼叫
明確呼叫解構函式很少是必要的。 不過,執行放在絕對位址上的物件的清理作業可能會很有用。 這些物件通常會使用採用 placement 自變數的使用者定義 new
運算符來配置。 運算子 delete
無法解除配置此記憶體,因為它不是從堆儲存區分配(如需詳細資訊,請參閱 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
明確定義解構函式、複製建構函式或複製指派運算符,可防止移動建構函式和移動指派運算元的隱含定義。 在此情況下,如果複製成本昂貴而無法提供移動作業,通常是遺漏了優化機會。