|
由显式调用析构函数引发的思考
想看干货的请直接到结尾~~~
前些日子使用Visual C++6.0 写程序,偶然发现在使用对象的成员时编译器提供的自动补全选项中,出现了如下情形(如右图):在自动补全选项中是有析构函数的(但是没有构造函数),这就让人产生疑问,是不是析构函数可以显式调用呢?经过测试后答案是肯定的,有代码如下:
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int x=0,int y=0)
{
this->x=x;
this->y=y;
cout<<"Object ("<<x<<","<<y<<") created!\n";
}
~Point()
{
cout<<"Object ("<<x<<","<<y<<") destroyed!\n";
}
};
int main()
{
Point p1(1,1),p2(2,2);
p1.Point::~Point();
Point p3(3,3);
return 0;
}
程序是可以正常运行的,输出结果:
以上结果表明,即使显式调用了析构函数,在对象生命周期结束的时候也会再调用一次析构函数,但是考虑到如果程序中申请了动态分配的内存空间(也就是堆内存),在程序末尾是否会因为释放两次而出现错误呢?(之前杨巧学长说不会,不过还是自己求证一下)于是对程序做以下修改:
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
int *a;
public:
Point(int x=0,int y=0)
{
this->x=x;
this->y=y;
this->a=new int;
cout<<"Object ("<<x<<","<<y<<") created!\n";
}
~Point()
{
delete (this->a);
cout<<"Object ("<<x<<","<<y<<") destroyed!\n";
}
};
int main()
{
Point p1(1,1),p2(2,2);
p1.Point::~Point();
Point p3(3,3);
return 0;
}
实践证明我们的担心是有必要的,程序在最后一句崩溃了:
这表明确实出现了多次delete的错误,在思考后想能否通过判断阻止这样的错误出现,析构函数代码修改如下:
~Point()
{
if(this->a!=NULL)
{
delete (this->a);
}
cout<<"Object ("<<x<<","<<y<<") destroyed!\n";
}
但是程序依然会崩溃,同时在调试过程中发现,在执行完p1.Point::~Point();之后,p1.a指针的值并不是NULL(0x00000000),而是保持原来的值不变。在主函数中加入普通的堆内存申请测试:
int *p=new int;
…
delete p;
调试窗口显示在delete p之后p指针指向并没有发生改变,看来我们对delete的理解有些偏差,delete之后的指针并没有置为NULL空指针,而是成为了所谓的“野指针”,当再次引用时会出现程序崩溃的问题。
在偶然中我为Point类添加了一个成员函数:
void func()
{
cout<<"func called!\n";
}
然后在主函数中这样调用:
int main()
{
int *p=new int;
Point p1(1,1),p2(2,2);
p1.Point::~Point();
delete p;
p1.func();
p2.func();
Point p3(3,3);
return 0;
}
程序运行的结果令我十分惊奇:
这表明虽然我们显示“释放”了对象p1,但是居然仍然可以调用p1的成员函数。
经过查阅资料(C++ Primer Plus)得知:编译器将我们自己定义的析构函数当作了普通成员函数处理,实际上不像构造函数,我们自己定义析构函数的时候,不会覆盖系统提供的析构函数;同时用户定义的析构函数,就是我们析构函数体内的内容,而系统提供的析构函数是负责对象的清理的。编译器允许显式调用析构函数,但是这种调用仅仅是调用了我们定义的析构函数(也就是当作普通成员函数处理的那个版本),并没有调用系统的析构函数。在本例中,显式“释放”p1时,实际上只是收回了p1.a所占的堆内存,p1.x,p1.y,p1.func()所占的栈内存并未释放,所以我们仍可以在显式“释放”p1之后调用它的func()方法,在对象生命周期结束(本例为程序结束)时,系统会自动调用对象的两种析构函数,以回收堆内存和栈内存(当然本例中尝试回收堆内存时出现了错误)。再结合delete的特性,将程序修改如下:
#include <iostream>
using namespace std;
class Point
{
private:
int x,y;
int *a;
public:
Point(int x=0,int y=0)
{
this->x=x;
this->y=y;
this->a=new int;
cout<<"Object ("<<x<<","<<y<<") created!\n";
}
~Point()
{
if(this->a!=NULL)
{
delete (this->a);
this->a=NULL;
}
cout<<"Object ("<<x<<","<<y<<") destroyed!\n";
}
void func()
{
cout<<"func called!\n";
}
};
int main()
{
int *p=new int;
Point p1(1,1),p2(2,2);
p1.Point::~Point();
delete p;
p1.func();
p2.func();
Point p3(3,3);
return 0;
}
终于,程序得以正常运行。因为我们在释放a之后将a的值置为了NULL,还在释放前判断了a是否为NULL,杜绝了重复释放的现象。
总结:
- 用delete释放指针所指空间,不改变指针的值(成为野指针),建议delete之后手动将指针置为NULL,并在delete之前用if语句加以判断,防止程序崩溃;
- 可以显式调用析构函数,但是编译器仅调用用户定义的部分,不调用系统隐含的析构函数,显式调用析构函数之后对象仍存在,只是释放了堆内存(如果有delete相关语句),仍然可以使用对象的其他部分,在对象生命周期结束时还会再调用系统隐含的析构函数以及用户定义的析构函数;
- 如想限制对象的生命周期或提前结束对象生命周期,请使用代码块(大括号),不要显式调用析构函数,但是显式调用析构函数确实可以“清理”诸如链表之类的对象(因为链表的节点都是动态分配的),当然,如果没有动态分配空间,自己编写析构函数的意义也不大。
2016年5月10日晚于科协基地 |