關於c++顯示呼叫解構函式的陷阱
一、文章來由
現在在寫一個專案,需要用到多叉樹儲存結構,但是在某個時候,我需要銷燬這棵樹,這意味著如果我新建了一個樹物件,我很可能在某處希望將這個物件的宣告週期終結,自然會想到顯示呼叫解構函式,但是就扯出來這麼大個陷阱。
二、原因
在瞭解為什麼不要輕易顯示呼叫解構函式之前,先來看看預備知識。
為了理解這個問題,我們必須首先弄明白“堆”和“棧”的概念。
1)堆區(heap) —— 一般由程式設計師分配釋放, 若程式設計師不釋放,程式結束時可能由OS回收。注意它與資料結構中的堆是兩回事,分配方式倒是類似於連結串列。
2)棧區(stack) —— 由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等。其操作方式類似於資料結構中的棧。
我們構造物件,往往都是在一段語句體中,比如函式,判斷,迴圈,還有就直接被一對“{}”包含的語句體。這個物件在語句體中被建立,在語句體結束的時候被銷燬。問題就在於,這樣的物件在生命週期中是存在於棧上的。也就是說,如何管理,是系統完成而程式設計師不能控制的。所以,即使我們呼叫了析構,在物件生命週期結束後,系統仍然會再呼叫一次解構函式,將其在棧上銷燬,實現真正的析構。
所以,如果我們在解構函式中有清除堆資料的語句,呼叫兩次意味著第二次會試圖清理已經被清理過了的,根本不再存在的資料!這是件會導致執行時錯誤的問題,並且在編譯的時候不會告訴你!
三、顯示呼叫帶來的後果
如果硬要顯示呼叫解構函式,不是不可以,但是會有如下3條後果:
1)顯式呼叫的時候,解構函式相當於的一個普通的成員函式;
2)編譯器隱式呼叫解構函式,如分配了對記憶體,顯式呼叫析構的話引起重複釋放堆記憶體的異常;
3)把一個物件看作佔用了部分棧記憶體,佔用了部分堆記憶體(如果申請了的話),這樣便於理解這個問題,系統隱式呼叫解構函式的時候,會加入釋放棧記憶體的動作(而堆記憶體則由使用者手工的釋放);使用者顯式呼叫解構函式的時候,只是單純執行解構函式內的語句,不會釋放棧記憶體,也不會摧毀物件。
用如下程式碼表示:
例1:
class aaa
{
public:
aaa(){}
~aaa(){cout<<"deconstructor" <<endl; } //解構函式
void disp(){cout<<"disp"<<endl;}
private:
char *p;
};
void main()
{
aaa a;
a.~aaa();
a.disp();
}
分析:
這樣的話,顯式兩次destructor,第一次析構相當於呼叫一個普通的成員函式,執行函式內語句,顯示第二次析構是編譯器隱式的呼叫,增加了釋放棧記憶體的動作,這個類未申請堆記憶體,所以物件乾淨地摧毀了,顯式+物件摧毀
例2:
class aaa
{
public:
aaa(){p = new char[1024];} //申請堆記憶體
~aaa(){cout<<"deconstructor"<<endl; delete []p;}
void disp(){cout<<"disp"<<endl;}
private:
char *p;
};
void main()
{
aaa a;
a.~aaa();
a.disp();
}
分析:
這樣的話,第一次顯式呼叫解構函式,相當於呼叫一個普通成員函式,執行函式語句,釋放了堆記憶體,但是並未釋放棧記憶體,物件還存在(但已殘缺,存在不安全因素);第二次呼叫解構函式,再次釋放堆記憶體(此時報異常),然後釋放棧記憶體,物件銷燬
四、奇葩的錯誤
系統在什麼情況下不會自動呼叫解構函式呢?顯然,如果物件被建立在堆上,系統就不會自動呼叫。一個常見的例子是new…delete組合。但是好在呼叫delete的時候,解構函式還是被自動呼叫了。很罕見的例外在於使用佈局new的時候,在delete設定的快取之前,需要顯式呼叫的解構函式,這實在是很少見的情況。
我在棧上建樹之後,顯示呼叫解構函式,物件地址任然存在,甚至還可以往裡面插入節點。。。
其實析構之前最好先看看堆上的資料是不是已經被釋放過了。
////////////////a.hpp
#ifndef A_HPP
#define A_HPP
#include <iostream>
using namespace std;
class A
{
private:
int a;
int* temp;
bool heap_deleted;
public:
A(int _a);
A(const A& _a);
~A();
void change(int x);
void show() const;
};
#endif
////////////a.cpp
#include "a.hpp"
A::A(int _a): heap_deleted(false)
{
temp = new int;
*temp = _a;
a = *temp;
cout<< "A Constructor!" << endl;
}
A::A(const A& _a): heap_deleted(false)
{
temp = new int;
*temp = _a.a;
a = *temp;
cout << "A Copy Constructor" << endl;
}
A::~A()
{
if ( heap_deleted == false){
cout << "temp at: " << temp << endl;
delete temp;
heap_deleted = true;
cout << "Heap Deleted!\n";
}
else {
cout << "Heap already Deleted!\n";
}
cout << "A Destroyed!" << endl;
}
void A::change(int x)
{
a = x;
}
void A::show() const
{
cout << "a = " << a << endl;
}
//////////////main.cpp
#include "a.hpp"
int main(int argc, char* argv[])
{
A a(1);
a.~A();
a.show();
cout << "main() end\n";
a.change(2);
a.show();
return 0;
}
五、小結
所以,一般不要自作聰明的去顯示呼叫解構函式。
—END—