那么如何进行妥善的内存回收呢?这需要交给析构函数来完成。
对象的销毁
-
析构函数:对象在销毁时,一定会调用析构函数
-
析构函数的作用:清理对象的数据成员申请的资源(堆空间)—— 析构函数并不负责清理数据成员(系统自动完成)
-
形式:【特殊的成员函数】
-
没有返回值,即使是void也没有
-
没有参数
-
函数名与类名相同,在类名之前需要加上一个波浪号~
-
-
析构函数只有一个(不能重载)
-
析构函数默认情况下 ,系统也会自动提供一个
-
当对象被销毁时 ,会自动调用析构函数【非常重要】
自定义析构函数
之前的例子中,我们没有显式定义出析构函数,但是没有问题,系统会自动提供一个默认的析构。
析构函数作为一个清理数据成员申请的堆空间的接口存在。
当数据成员中有指针时,创建一个对象,会申请堆空间,销毁对象时默认析构不够用了(造成内存泄漏),此时就需要我们自定义析构函数。在析构函数中定义堆空间上内存回收的机制,就不会发生内存泄漏。
同样以Computer类为例
class Computer {
public:
Computer(const char * brand, double price)
: _brand(new char[strlen(brand) + 1]())
, _price(price)
{}
~Computer()
{
if(_brand){
delete [] _brand;
_brand = nullptr//设为空指针,安全回收
}
cout << "~Computer()" << endl;
}
private:
char * _brand;
double _price;
};
析构函数:如果指针成员申请了堆空间,就回收这片空间,并将指针成员设为空指针,进行安全回收。
析构函数的规范写法为什么这样写呢?实际上,如果类中没有指针数据成员,即数据成员没有申请堆空间的情况下,默认的析构函数就够用了。
(1)如果没有进行安全回收这一步会引发很多问题,此时我们没有学习类与对象的更多知识,可以做个简单小实验,看看会发生什么情况,思考一下原因
~Computer()
{
if(_brand){
delete [] _brand;
//_brand = nullptr//设为空指针,安全回收
}
cout << "~Computer()" << endl;
}
void test0(){
Computer pc("apple",12000);
pc.print();
pc.~Computer();//手动调用析构函数
}
——第一次手动调用析构函数时已经回收了这片堆空间,但是_brand存的地址值依然有效,当对象销毁时自动调用析构函数,依然会进入if语句,再一次试图回收这片空间,发生double free错误。
(2)如果没有对指针成员的判断,可能会有delete一个空指针的情况。尽管一些平台,delete本身会自动检查对象是否为空,如果为空就不做操作,但是在其他的一些平台这样做可能会导致风险,所以请按照规范去定义析构函数。
注意:对象被销毁,一定会调用析构函数;
调用了析构函数,对象并不会被销毁。
对上面这句话的解释:对象是存放在栈上的和堆上的,析构函数是用来释放存放在堆上的空间的,当调用析构函数,被销毁的是堆上的对象但对象真正被销毁时将栈上的对象销毁,对象被销毁说明系统自动调用析构函数来释放堆上的空间。
上述例子中手动调用了析构函数,发现之后又自动调用了一次析构函数。
那么在手动调用析构函数之后,再次调用print函数,看看会发生什么?
Computer pc("apple",12000);
pc.~Computer();
pc.print();
发现程序在print执行时尝试对char型空指针进行输出,导致程序中断。
结论:不建议手动调用析构函数,因为容易导致各种问题,应该让析构函数自动被调用,这也是为什么要按规范写析构函数。
构造函数和析构函数的调用时机(重点)
-
对于全局定义的对象,每当程序开始运行,在主函数 main 接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时,自动调用全局对象的析构函数。
-
对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
-
对于关键字 static 定义的静态对象,当程序流程到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
-
对于用 new 运算符创建的堆对象,每当创建该对象时调用构造函数,在使用 delete 删除该对象时,调用析构函数。
堆上对象的释放顺序,先释放lenove,因为先释放前面后面就找不到了。在使用 delete 删除该对象时,调用析构函数。
如果是对象存储在同一内存分区,调用析构函数的顺序与调用构造函数的顺序相反,也就是先创建的后销毁。(和栈没有关系只是和栈的数据变化顺序一样)