zoukankan      html  css  js  c++  java
  • 关于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!
    ";
        }
        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;
    }

    五、小结

    所以,一般不要自作聪明的去显示调用析构函数。

  • 相关阅读:
    Ceph实验室:第六课:Ceph运维之横向扩展
    Ceph实验室:第五课:Ceph运维之换盘
    百度2014软件开发工程师笔试题详解 (转)
    阿里巴巴2014笔试题详解(9月22北京)(转)
    阿里巴巴2014秋季校园招聘-软件研发工程师笔试题详解(转)
    腾讯的2014年校招的软开笔试题(转)
    typedef与define区别
    java流总结(转)
    java 流 复制,重命名,删除目录
    java 流 读
  • 原文地址:https://www.cnblogs.com/zsq1993/p/5838034.html
Copyright © 2011-2022 走看看