显式析构函数的陷阱及解决方案(转)
为了理解这个问题,我们必须首先弄明白“堆”和“栈”的概念。
堆区(heap) —— 一般由程序员分配释放(如用new申请的内存), 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式 倒是类似于链表。
栈区(stack)—— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
我们构造对象,往往都是在一段语句体中,比如函数,判断,循环,还有就直接被一对“{}”包含的语句体。这个对象在语句体中被创建,在语句体结束的时候被 销毁。问题就在于,这样的对象在生命周期中是存在于栈上的。也就是说,如何管理,是系统完成而程序员不能控制的。所以,即使我们调用了析构,在对象生命周 期结束后,系统仍然会再调用一次析构函数,将其在栈上销毁,实现真正的析构。
所以,如果我们在析构函数中有清除堆数据的语句,调用两次意味着第二次会试图清理已经被清理过了的,根本不再存在的数据!这是件会导致运行时错误的问题,并且在编译的时候不会告诉你!
堆区(heap) —— 一般由程序员分配释放(如用new申请的内存), 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式 倒是类似于链表。
栈区(stack)—— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
我们构造对象,往往都是在一段语句体中,比如函数,判断,循环,还有就直接被一对“{}”包含的语句体。这个对象在语句体中被创建,在语句体结束的时候被 销毁。问题就在于,这样的对象在生命周期中是存在于栈上的。也就是说,如何管理,是系统完成而程序员不能控制的。所以,即使我们调用了析构,在对象生命周 期结束后,系统仍然会再调用一次析构函数,将其在栈上销毁,实现真正的析构。
所以,如果我们在析构函数中有清除堆数据的语句,调用两次意味着第二次会试图清理已经被清理过了的,根本不再存在的数据!这是件会导致运行时错误的问题,并且在编译的时候不会告诉你!
1。显式调用的时候,析构函数相当于的一个普通的成员函数
2。编译器隐式调用析构函数,如分配了对内存,显式调用析构的话引起重复释放堆内存的异常
3。把一个对象看作占用了部分栈内存,占用了部分堆内存(如果申请了的话),这样便于理解这个问题
系统隐式调用析构函数的时候,会加入释放栈内存的动作(而堆内存则由用户手工的释放)
用户显式调用析构函数的时候,只是单纯执行析构函数内的语句,不会释放栈内存,摧毁对象
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,第一次析构相当于调用一个普通的成员函数,执行函数内语句,显示
第二次析构是编译器隐式的调用,增加了释放栈内存的动作,这个类未申请堆内存,所以对象干净地摧毁了,
显式+对象摧毁;
#include <iostream>
using namespace std;
class aaa
{
public:
aaa(){p = new int(3);}
~aaa()
{
cout<<"deconstructor"<<endl;
delete p;//手工释放堆内存
}
void disp(){cout<<"disp"<<endl;}
private:
int *p;
};
void main()
{
aaa a;
a.~aaa();//显示调用析构函数
a.disp();
}
这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);
第二次调用析构函数,再次释放堆内存(此时报异常),然后释放栈内存,对象销毁
2。编译器隐式调用析构函数,如分配了对内存,显式调用析构的话引起重复释放堆内存的异常
3。把一个对象看作占用了部分栈内存,占用了部分堆内存(如果申请了的话),这样便于理解这个问题
系统隐式调用析构函数的时候,会加入释放栈内存的动作(而堆内存则由用户手工的释放)
用户显式调用析构函数的时候,只是单纯执行析构函数内的语句,不会释放栈内存,摧毁对象
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,第一次析构相当于调用一个普通的成员函数,执行函数内语句,显示
第二次析构是编译器隐式的调用,增加了释放栈内存的动作,这个类未申请堆内存,所以对象干净地摧毁了,
显式+对象摧毁;
#include <iostream>
using namespace std;
class aaa
{
public:
aaa(){p = new int(3);}
~aaa()
{
cout<<"deconstructor"<<endl;
delete p;//手工释放堆内存
}
void disp(){cout<<"disp"<<endl;}
private:
int *p;
};
void main()
{
aaa a;
a.~aaa();//显示调用析构函数
a.disp();
}
这样的话,第一次显式调用析构函数,相当于调用一个普通成员函数,执行函数语句,释放了堆内存,但是并未释放栈内存,对象还存在(但已残缺,存在不安全因素);
第二次调用析构函数,再次释放堆内存(此时报异常),然后释放栈内存,对象销毁
解决方案:添加一个bool变量(类成员)
#include <iostream>
using namespace std;
class aaa
{
public:
aaa()
{
p = new int(3);
heap_deleted=false;
}
~aaa()
{
cout<<"deconstructor"<<endl;
if(heap_deleted==false)//判断堆是否已被释放
{
delete p;
heap_deleted=true;
}
}
void disp(){cout<<"disp"<<endl;}
private:
int *p;
bool heap_deleted;//标志,以便判断堆是否被释放
};
void main()
{
aaa a;
a.~aaa();//显示调用析构函数
a.disp();
cout<<"main_end"<<endl;
}
using namespace std;
class aaa
{
public:
aaa()
{
p = new int(3);
heap_deleted=false;
}
~aaa()
{
cout<<"deconstructor"<<endl;
if(heap_deleted==false)//判断堆是否已被释放
{
delete p;
heap_deleted=true;
}
}
void disp(){cout<<"disp"<<endl;}
private:
int *p;
bool heap_deleted;//标志,以便判断堆是否被释放
};
void main()
{
aaa a;
a.~aaa();//显示调用析构函数
a.disp();
cout<<"main_end"<<endl;
}