避免内存泄漏:掌握C++中类对象销毁与析构函数的关键技巧

那么如何进行妥善的内存回收呢?这需要交给析构函数来完成。

对象的销毁

  1. 析构函数:对象在销毁时,一定会调用析构函数

  2. 析构函数的作用:清理对象的数据成员申请的资源(堆空间)—— 析构函数并不负责清理数据成员(系统自动完成)

  3. 形式:【特殊的成员函数】

    • 没有返回值,即使是void也没有

    • 没有参数

    • 函数名与类名相同,在类名之前需要加上一个波浪号~

  4. 析构函数只有一个(不能重载)

  5. 析构函数默认情况下 ,系统也会自动提供一个

  6. 当对象被销毁时 ,会自动调用析构函数【非常重要】

自定义析构函数

之前的例子中,我们没有显式定义出析构函数,但是没有问题,系统会自动提供一个默认的析构。

析构函数作为一个清理数据成员申请的堆空间的接口存在。

当数据成员中有指针时,创建一个对象,会申请堆空间,销毁对象时默认析构不够用了(造成内存泄漏),此时就需要我们自定义析构函数。在析构函数中定义堆空间上内存回收的机制,就不会发生内存泄漏。

同样以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;
};

image-20240307153116460

析构函数:如果指针成员申请了堆空间,就回收这片空间,并将指针成员设为空指针,进行安全回收。

image-20240307160338793

析构函数的规范写法为什么这样写呢?实际上,如果类中没有指针数据成员,即数据成员没有申请堆空间的情况下,默认的析构函数就够用了。

(1)如果没有进行安全回收这一步会引发很多问题,此时我们没有学习类与对象的更多知识,可以做个简单小实验,看看会发生什么情况,思考一下原因

~Computer()
{
	if(_brand){
    	delete [] _brand;
        //_brand = nullptr//设为空指针,安全回收
       }
	cout << "~Computer()" << endl;
}

void test0(){
    Computer pc("apple",12000);
    pc.print();
    pc.~Computer();//手动调用析构函数
}

image-20240307160835943

——第一次手动调用析构函数时已经回收了这片堆空间,但是_brand存的地址值依然有效,当对象销毁时自动调用析构函数,依然会进入if语句,再一次试图回收这片空间,发生double free错误。

(2)如果没有对指针成员的判断,可能会有delete一个空指针的情况。尽管一些平台,delete本身会自动检查对象是否为空,如果为空就不做操作,但是在其他的一些平台这样做可能会导致风险,所以请按照规范去定义析构函数。

注意:对象被销毁,一定会调用析构函数;

调用了析构函数,对象并不会被销毁。

对上面这句话的解释:对象是存放在栈上的和堆上的,析构函数是用来释放存放在堆上的空间的,当调用析构函数,被销毁的是堆上的对象但对象真正被销毁时将栈上的对象销毁,对象被销毁说明系统自动调用析构函数来释放堆上的空间。

上述例子中手动调用了析构函数,发现之后又自动调用了一次析构函数。

那么在手动调用析构函数之后,再次调用print函数,看看会发生什么?

Computer pc("apple",12000);
pc.~Computer();
pc.print();

发现程序在print执行时尝试对char型空指针进行输出,导致程序中断。

image-20240307162401650

结论:不建议手动调用析构函数,因为容易导致各种问题,应该让析构函数自动被调用,这也是为什么要按规范写析构函数。

构造函数和析构函数的调用时机(重点)

  1. 对于全局定义的对象,每当程序开始运行,在主函数 main 接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时,自动调用全局对象的析构函数。

  2. 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。

  3. 对于关键字 static 定义的静态对象,当程序流程到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。

  4. 对于用 new 运算符创建的堆对象,每当创建该对象时调用构造函数,在使用 delete 删除该对象时,调用析构函数。

image-20240307163819303

image-20240307164010234

堆上对象的释放顺序,先释放lenove,因为先释放前面后面就找不到了。在使用 delete 删除该对象时,调用析构函数

如果是对象存储在同一内存分区,调用析构函数的顺序与调用构造函数的顺序相反,也就是先创建的后销毁。(和栈没有关系只是和栈的数据变化顺序一样)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值