析構函數 (C++)

最近發現自己對析構函數的認知有一定的問題,因為之前有在使用placement new時主動調用對象的析構函數,所以覺得析構函數只是個普通的成員函數,調用的時候只會執行自己方法體內的代碼內容,而回收內存則是由於生命週期到了操作系統自動回收的。不過昨天突然在想,那如果是派生類的話,主動調用析構函數的話,基類的內容怎麼回收呢。於是,發現了自己長期以來的基礎錯誤,也說明了自己還是需要不斷鞏固基礎啊。話不多說,直接開始介紹。

“析構函數”是構造函數的反向函數。 在銷燬(釋放)對象時將調用它們。 通過在類名前面放置一個波形符 (~) 將函數指定為類的析構函數。 例如,聲明 String 類的析構函數:~String()。

在 /clr 編譯中,析構函數在釋放託管和非託管資源方面發揮了特殊作用。 有關詳細信息,請參閱Visual C++ 中的析構函數和終結器。

析構函數通常用於在不再需要某個對象時“清理”此對象。 請考慮 String 類的以下聲明:

 1 // spec1_destructors.cpp 
2 #include
3
4 class String {
5 public:
6 String( char *ch ); // Declare constructor
7 ~String(); // and destructor.
8 private:
9 char *_text;
10 size_t sizeOfText;
11 };
12
13 // Define the constructor.
14 String::String( char *ch ) {
15 sizeOfText = strlen( ch ) + 1;
16
17 // Dynamically allocate the correct amount of memory.
18 _text = new char[ sizeOfText ];
19
20 // If the allocation succeeds, copy the initialization string.
21 if( _text )
22 strcpy_s( _text, sizeOfText, ch );
23 }
24
25 // Define the destructor.
26 String::~String() {

27 // Deallocate the memory that was previously reserved
28 // for this string.
29 if (_text)
30 delete[] _text;
31 }
32
33 int main() {
34 String str("The piper in the glen...");
35 }

在前面的示例中,析構函數 String::~String 使用 delete 運算符來動態釋放為文本存儲分配的空間。

析構函數是具有與類相同的名稱但前面是波形符 (~) 的函數

該語法的第一種形式用於在類聲明中聲明或定義的析構函數;第二種形式用於在類聲明的外部定義的析構函數。

多個規則管理析構函數的聲明。 析構函數:

  • 不接受參數。
  • 無法指定任何返回類型(包括 void)。
  • 無法使用 return 語句返回值。
  • 無法聲明為 const、volatile 或 static。 但是,可以為聲明為 const
    、volatile 或 static 的對象的析構調用它們。
  • 可以聲明為 virtual。 通過使用虛擬析構函數,無需知道對象的類型即可銷燬對象 - 使用虛函數機制調用該對象的正確析構函數。 請注意,析構函數也可以聲明為抽象類的純虛函數。

使用構造函數

當下列事件之一發生時,將調用析構函數:

  • 使用 delete 運算符顯式解除分配了使用 new 運算符分配的對象。 使用 delete 運算符解除分配對象時,將為“大多數派生對象”或為屬於完整對象,但不是表示基類的子對象的對象釋放內存。 此“大多數派生對象”解除分配一定僅對虛擬析構函數有效。 在類型信息與實際對象的基礎類型不匹配的多重繼承情況下,取消分配可能失敗。
  • 具有塊範圍的本地(自動)對象超出範圍。
  • 臨時對象的生存期結束。
  • 程序結束,並且存在全局或靜態對象。
  • 使用析構函數的完全限定名顯式調用了析構函數。 (有關詳細信息,請參閱顯式析構函數調用。)

前面的列表中所述的情況將確保所有對象均可通過用戶定義的方法進行銷燬。

如果基類或數據成員有一個可訪問的析構函數,並且派生類未聲明析構函數,則編譯器將生成一個析構函數。 此編譯器生成的析構函數將為派生類型的成員調用基類析構函數和析構函數。 默認析構函數是公共的。 (有關可訪問性的詳細信息,請參閱基類的訪問修飾符。)

析構函數可以隨意調用類成員函數和訪問類成員數據。 從析構函數調用虛函數時,調用的函數是當前正在銷燬的類的函數。 (有關詳細信息,請參閱析構函數的順序。)

析構函數的使用有兩個限制。 第一個限制是您無法採用析構函數的地址。 第二個是派生類不會繼承其基類的析構函數。 相反,如前所釋,它們始終重寫基類的析構函數。

析構的順序

當對象超出範圍或被刪除時,其完整析構中的事件序列如下所示:

  1. 將調用該類的析構函數,並且會執行該析構函數的主體。
  2. 按照非靜態成員對象的析構函數在類聲明中的顯示順序的相反順序調用這些函數。 用於這些成員的構造的可選成員優化列表不影響構造或析構的順序。 (有關初始化成員的詳細信息,請參閱初始化基和成員。)
  3. 非虛擬基類的析構函數以聲明的相反順序被調用。
  4. 虛擬基類的析構函數以聲明的相反順序被調用。
 1 // order_of_destruction.cpp 
2 #include
3
4 struct A1 { virtual ~A1() { printf("A1 dtor\n"); } };
5 struct A2 : A1 { virtual ~A2() { printf("A2 dtor\n"); } };
6 struct A3 : A2 { virtual ~A3() { printf("A3 dtor\n"); } };
7
8 struct B1 { ~B1() { printf("B1 dtor\n"); } };
9 struct B2 : B1 { ~B2() { printf("B2 dtor\n"); } };
10 struct B3 : B2 { ~B3() { printf("B3 dtor\n"); } };
11
12 int main() {
13 A1 * a = new A3;

14 delete a;
15 printf("\n");
16
17 B1 * b = new B3;
18 delete b;
19 printf("\n");
20
21 B3 * b2 = new B3;
22 delete b2; //或者b2->~B3(),結果相同
23 }
24
25 Output: A3 dtor
26 A2 dtor
27 A1 dtor
28
29 B1 dtor
30
31 B3 dtor
32 B2 dtor
33 B1 dtor

虛擬基類

按照與虛擬基類在定向非循環圖形中顯示的順序的相反順序調用這些虛擬基類的析構函數(深度優先、從左到右、後序遍歷)。 下圖描述了繼承關係圖。

析構函數 (C++)

演示虛擬基類的繼承關係圖

下面列出了圖中顯示的類的類頭。

1 class A 
2 class B
3 class C : virtual public A, virtual public B
4 class D : virtual public A, virtual public B
5 class E : public C, public D, virtual public B

為了確定 E 類型的對象的虛擬基類的析構順序,編譯器將通過應用以下算法來生成列表:

  1. 向左遍歷關係圖,並從關係圖中的最深點開始(在此示例中,為 E)。
  2. 執行左移遍歷,直到訪問了所有節點。 記下當前節點的名稱。
  3. 重新訪問上一個節點(向下並向右)以查明要記住的節點是否為虛擬基類。
  4. 如果記住的節點是虛擬基類,請瀏覽列表以查看是否已將其輸入。 如果它不是虛擬基類,則將其忽略。
  5. 如果記住的節點尚未包含在列表中,請將其添加到列表的底部。
  6. 向上遍歷關係圖並沿下一個路徑向右遍歷。
  7. 轉到步驟 2。
  8. 在用完最後一個向上路徑時,請記下當前節點的名稱。
  9. 轉到步驟 3。
  10. 繼續執行此過程,直到底部節點再次成為當前節點。

因此,對於 E 類,析構順序為:

  1. 非虛擬基類 E。
  2. 非虛擬基類 D。
  3. 非虛擬基類 C。
  4. 虛擬基類 B。
  5. 虛擬基類 A。

此過程將生成唯一項的有序列表。 任何類名均不會出現兩次。 在構造列表後,將以相反的順序遍歷該列表,並且將調用列表中每個類(從最後一個到第一個)的析構函數。

如果某個類中的構造函數或析構函數依賴於要先創建或保留更長時間的另一個組件(例如,如果 A 的析構函數(上圖中所示)依賴於執行其代碼時仍存在 B),則構造或析構的順序特別重要,反之亦然。

繼承關係圖中各個類之間的這種相互依賴項本質上是危險的,因為稍後派生類可以更改最左邊的路徑,從而更改構造和析構的順序。

非虛擬基類

按照相反的順序(按此順序聲明基類名稱)調用非虛擬基類的析構函數。 考慮下列類聲明:

1 class MultInherit : public Base1, public Base2 
2 ...

在前面的示例中,先於 Base2 的析構函數調用 Base1 的析構函數。

顯式析構函數調用

很少需要顯式調用析構函數。 但是,對置於絕對地址的對象進行清理會很有用。 這些對象通常使用採用位置參數的用戶定義的 new 運算符進行分配。 delete 運算符不能釋放該內存,因為它不是從自由存儲區分配的(有關詳細信息,請參閱 new 和 delete 運算符)。 但是,對析構函數的調用可以執行相應的清理。 若要顯式調用 s 類的對象 String 的析構函數,請使用下列語句之一:

1 s.String::~String(); // Nonvirtual call 
2 ps->String::~String(); // Nonvirtual call
3
4 s.~String(); // Virtual call
5 ps->~String(); // Virtual call

可以使用對前面顯示的析構函數的顯式調用的表示法,無論類型是否定義了析構函數。 這允許您進行此類顯式調用,而無需瞭解是否為此類型定義了析構函數。 顯式調用析構函數,其中未定義的析構函數無效。

析構函數 (C++)


分享到:


相關文章: