防止内存泄漏的神兵利器 — 智能指针

1.内存泄漏

1.1什么是内存泄漏

当我们在写C/C++程序的时候,难免会出现内存泄漏的错误,因为C/C++不像Java语言那样,拥有自己的垃圾回收机制,C/C++中对于资源的管理,完全交给程序员自己打理,也就是说使用C/C++的程序员可以直接和内存打交道,写出来的程序效率自然比其他语言的运行速度更快,这是C++的优点,但同样也是C++的缺点,因为,我们难以保证我们是否正确释放了不在使用的资源。比如:当我们因为疏忽大意而忘记释放不在使用的程序;又或者是我们记得释放不再使用的资源,但,因为程序中的执行流乱跳而导致程序没有执行到释放资源的代码;这个时候就会造成内存泄漏。内存泄漏简单来说就是 未释放不再使用的内存资源。

内存是一种有限的资源,使用完之后放回原处(还给操作系统),当程序中其他地方还需要使用的时候,直接向操作系统申请即可;但是,如果使用完之后没有放回原处(未还给操作系统),当其他程序向操作系统申请内存空间的时候,操作系统就会左拼右凑给该程序分配一块内存空间,但是当有非常巨大的内存空间没有还给操作系统时,操作系统就会很尴尬的说,“不好意思,没有,哪个谁谁谁还没还给我呢”,这个时候就会造成程序运行缓慢,严重的话还会造成程序卡死。所以我们应当避免内存泄漏。

1.2如何防止内存泄漏

通常来说,我们申请的内存资源,在不使用的时候记得释放即可,类似于一下代码

int* func()
{
	int* ptr = new int[10]; // 申请资源
	return ptr;
}

int main()
{
	int* ptr = func(); 
	delete[] ptr;      // 释放资源

	return 0;
}

但是总有一些特殊情况,比如在使用异常的时候,当捕获异常之后,程序执行流直接跳转到匹配的catch语句块中执行,如果在这之间跳过了释放资源的代码语句,就会造成资源泄漏问题;如以下代码:

int divi(int a,int b)
{
	if (0 == b)
		throw "处零错误";
	
	return a / b;
}

void func()
{
	int* ptr = new int;
	divi(4,0);

	cout << "释放资源" << endl;
	delete ptr;
}

int main()
{
	try
	{
		func();
	}
	catch (...)
	{
		cout << "出现除零错误" << endl;
	}

	return 0;
}

可以看出,当出现除0错误的时候,程序跳过了释放资源的语句,造成程序泄漏,所以光记得释放资源也不一定能避免内存泄漏问题,这个时候,C++的前辈们就引入了新的机制,通过智能指针来管理资源。

2.RAII和智能指针的关系

RAII(Resource Acquisition Is Initialization)是一种编程技术,即资源获得即初始化;智能指针是利用这个技术所实现的具体产物;所以学习智能指针之前,很有必要了解一下RAII的思想;RAII的核心思想是将资源的获取(初始化)与对象的构造绑定,将资源的释放与对象的析构绑定,从而把管理一份资源的责任托管给一个对象; 利用C++的作用域和析构函数的特性来自动管理资源确保资源在不再需要时能够被自动释放,从而避免了资源泄漏和其他资源管理错误。

我们可以类比于局部的临时变量来理解,临时变量只在其作用域有效,当出了作用域就销毁了,这是在栈区上开辟资源的特性;但是我们申请的资源是在堆区的,堆区上的资源 生命周期是随进程的,不会像栈区上的资源那样自动释放,那如果通过栈区上的对象来管理堆区上的资源,是不是就可以保证资源的自动销毁呢?没错,这就是实现智能指针的思想。

RAII的简单代码如下:

template 
class RAII_test
{
public:
	smart_ptr(T* ptr)
		:_ptr(ptr) // 资源的获取与对象的构造函数绑定
	{}

	~smart_ptr()   // 资源的释放与对象的析构函数绑定
	{
		delete _ptr;
	}
private:
	T* _ptr;
};

上述代码只是展示一下RAII思想,还不能称为智能指针,因为它还不能像指针一样使用;要想实现智能指针,还需要使其具有指针的行为,如:解引用,通过箭头访问。简单的智能指针代码如下:

template
class SmartPtr
{
public:
	// 1.RAII
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}
	// 2.像指针一样使用
	T& operator*() {return *_ptr;}
	T* operator->() {return _ptr;}

private:
	T* _ptr;
};

3.C++标准库中的智能指针

3.1标准库中智能指针简介

智能指针可谓是解决内存泄漏的神兵利器,C++标准库中提供了四种智能指针,分别是auto_ptr、unique_ptr、shared_ptr、weak_ptr;这时,你就应该有一个大大的疑问了,为什么C++的标准库中要提供四种智能指针呢?提供一个不就好了吗,毕竟他们都是进行管理资源的。这其实和智能指针的拷贝有关。

智能指针之间的拷贝,不同于string,vector这些类的拷贝,这些类拷贝完之后,我们希望拷贝出来的对象拥有自己的资源,所以是深拷贝。而智能指针拷贝完成之后,我们希望拷贝的智能指针应该指向原来的资源,而不是指向自己独有的资源,所以智能指针之间的拷贝应该是浅;但是这就会造成析构两次的问题,第一次析构正常析构,第二次析构的指针就变成野指针了,析构野指针,程序崩溃。所以为了解决智能指针之间的拷贝问题,标准库中提供了四个智能指针。

各个智能指针解决拷贝问题的思想:

        auto_ptr:auto_ptr的实现思想是 管理权转移;但是auto_ptr很坑,会导致被拷贝对象置空,一般不建议使用。

        unique_ptr:unique_ptr的实现思想是 禁止拷贝;适用于不需要拷贝的场景。

        shared_ptr:shared_ptr的实现思想是 通过引用计数来管理资源的释放;允许自由拷贝,但是使用的时候要注意避免循环引用的问题。

        weak_ptr:weak_ptr主要 用来解决shared_ptr中的循环引用问题

auto_ptr和unique_ptr比较简单,下面主要讲解一下shared_ptr。

3.2shared_ptr的引用计数的实现

我们知道shared_ptr是通过引用计数来支持拷贝的,抱着 “知其然,知其所以然” 的态度,我们一起来了解一下shared_ptr是如何通过引用计数来解决对象之间的拷贝问题。

大体思想就是一份资源配一个引用计数,无论多少个对象管理这份资源,都只有一个引用计数。具体实现就是,每个对象存一个找的这个引用计数的指针。如下图所示:​​​​​​​防止内存泄漏的神兵利器 — 智能指针_第1张图片

shared_ptr具体实现可以参考下面这份简单的代码: 

template
class myshared_ptr
{
public:
	// RAII
	myshared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	myshared_ptr(const myshared_ptr& sp)
	{
		_ptr = sp._ptr;
		_pcount = sp._pcount;

		// 拷贝时++计数
		++(*_pcount);
	}
	myshared_ptr& operator=(const myshared_ptr& sp)
	{
		//if (this != &sp)
		if (_ptr != sp._ptr)
		{
			release();

			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}

		return *this;
	}
	void release()
	{
		// 说明最后一个管理对象析构了,可以释放资源了
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}
    // 析构时,--计数,计数减到0,
	~myshared_ptr(){release();}

	// 像指针一样
	T& operator*(){return *_ptr;}
	T* operator->(){return _ptr;}
private:
	T* _ptr;
	int* _pcount;
};

3.3shared_ptr的循环引用问题

shared_ptr千般好万般好,但是在特殊场景下,也会存在缺陷,如以下代码:

struct Node
{
	int _val;
	myshared_ptr _next;
	myshared_ptr _prev;

	Node(int val = 0)
		:_val(val)
	{}

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

int main()
{
	myshared_ptr ptr1(new Node(1));
	myshared_ptr ptr2(new Node(2));

	ptr1->_next = ptr2;
	ptr2->_prev = ptr1;

	return 0;
}

 上面代码的场景如下图:

防止内存泄漏的神兵利器 — 智能指针_第2张图片

分析上述场景可知,该场景为有两个结点,结点中的myshared_ptr互相指向对方;析构的时候,ptr2指向的结点先析构,因为ptr1中的成员变量_next指向ptr2所指向的结点,所以引用计数减到1,右侧节点不会释放;析构ptr1指向的结点时,因为ptr2的成员变量_prev指向当前节点,引用计数减到1,左侧结点也不会释放。最终造成两个结点都不会释放,这就是shared_ptr在该场景下的循环引用问题。

为了解决循环引用问题,C++标准库提供了weak_ptr,将定义结点的代码改为下面这份代码即可解决问题:

struct Node
{
	int _val;
	std::weak_ptr _next;
	std::weak_ptr _prev;

	Node(int val = 0)
		:_val(val)
	{}

	~Node()
	{
		cout << "~Node()" << endl;
	}
};

当使用weak_ptr时,在该场景下,ptr1和ptr2所指向的结点的引用计数,不会因为对方的成员变量weak_ptr类型的对象指向彼此而增加。使用weak_ptr后场景如下:

防止内存泄漏的神兵利器 — 智能指针_第3张图片

当对方中的_prev和_next是weak_ptr时,引用计数不会增加,析构的时候,ptr2先析构,引用计数减到0,析构ptr2指向的结点;ptr1再析构,同理,引用计数减到0,析构ptr1所指向的结点;最终,两个结点都析构了。

你可能感兴趣的:(C/C++,jvm)