程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 關於c++顯示調用析構函數的陷阱

關於c++顯示調用析構函數的陷阱

編輯:C++入門知識

關於c++顯示調用析構函數的陷阱


一、文章來由

現在在寫一個項目,需要用到多叉樹存儲結構,但是在某個時候,我需要銷毀這棵樹,這意味著如果我新建了一個樹對象,我很可能在某處希望將這個對象的聲明周期終結,自然會想到顯示調用析構函數,但是就扯出來這麼大個陷阱。

二、原因

在了解為什麼不要輕易顯示調用析構函數之前,先來看看預備知識。
為了理解這個問題,我們必須首先弄明白“堆”和“棧”的概念。

1)堆區(heap) —— 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。

2)棧區(stack) —— 由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。

我們構造對象,往往都是在一段語句體中,比如函數,判斷,循環,還有就直接被一對“{}”包含的語句體。這個對象在語句體中被創建,在語句體結束的時候被銷毀。問題就在於,這樣的對象在生命周期中是存在於棧上的。也就是說,如何管理,是系統完成而程序員不能控制的。所以,即使我們調用了析構,在對象生命周期結束後,系統仍然會再調用一次析構函數,將其在棧上銷毀,實現真正的析構。

所以,如果我們在析構函數中有清除堆數據的語句,調用兩次意味著第二次會試圖清理已經被清理過了的,根本不再存在的數據!這是件會導致運行時錯誤的問題,並且在編譯的時候不會告訴你!

三、顯示調用帶來的後果

如果硬要顯示調用析構函數,不是不可以,但是會有如下3條後果:

1)顯式調用的時候,析構函數相當於的一個普通的成員函數

2)編譯器隱式調用析構函數,如分配了對內存,顯式調用析構的話引起重復釋放堆內存的異常

3)把一個對象看作占用了部分棧內存,占用了部分堆內存(如果申請了的話),這樣便於理解這個問題,系統隱式調用析構函數的時候,會加入釋放棧內存的動作(而堆內存則由用戶手工的釋放);用戶顯式調用析構函數的時候,只是單純執行析構函數內的語句,不會釋放棧內存,也不會摧毀對象

用如下代碼表示:

例1:

class aaa
{
public:
    aaa(){}
    ~aaa(){cout<

分析:

這樣的話,顯式兩次destructor,第一次析構相當於調用一個普通的成員函數,執行函數內語句,顯示第二次析構是編譯器隱式的調用,增加了釋放棧內存的動作,這個類未申請堆內存,所以對象干淨地摧毀了,顯式+對象摧毀

例2:

class aaa
{
public:
    aaa(){p = new char[1024];} //申請堆內存
    ~aaa(){cout<

分析:

這樣的話,第一次顯式調用析構函數,相當於調用一個普通成員函數,執行函數語句,釋放了堆內存,但是並未釋放棧內存,對象還存在(但已殘缺,存在不安全因素);第二次調用析構函數,再次釋放堆內存(此時報異常),然後釋放棧內存,對象銷毀

四、奇葩的錯誤

系統在什麼情況下不會自動調用析構函數呢?顯然,如果對象被建立在堆上,系統就不會自動調用。一個常見的例子是new…delete組合。但是好在調用delete的時候,析構函數還是被自動調用了。很罕見的例外在於使用布局new的時候,在delete設置的緩存之前,需要顯式調用的析構函數,這實在是很少見的情況。

我在棧上建樹之後,顯示調用析構函數,對象地址任然存在,甚至還可以往裡面插入節點。。。

其實析構之前最好先看看堆上的數據是不是已經被釋放過了。

////////////////a.hpp
#ifndef A_HPP
#define A_HPP

#include 
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!
;
    }
    else {
        cout << Heap  already Deleted!
;
    }

    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
;
    a.change(2);
    a.show();

    return 0;
}

五、小結

所以,一般不要自作聰明的去顯示調用析構函數。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved