智能指针简述

为什么要智能指针

通过普通指针,我们能轻松访问到数据的地址。但是,我们往往会忘记在合适的地方释放它。那么有没有一种方法,来辅助我们释放指针呢?答案是有的,就是接下来要介绍的智能指针。

栈空间和堆空间

我们先引入两个概念,栈空间和堆空间。简单来说,栈空间由系统分配和释放,堆空间由程序猿申请和释放。我们可以利用栈的特性来帮助释放指针。

class HelpPtr
{
public:
	HelpPtr() {}
	~HelpPtr() {}
};
int main()
{
	HelpPtr ptr;
	return 0;
}

先看上面的代码,在main函数中,我们申明了一个局部变量。根据栈空间的特性,在函数返回后,ptr是不是就自动释放了呢,释放时会调用析构函数。发散一下,如果ptr中保存了一个指针,在析构函数中将指针delete,我们没有主动调用delete,HelpPtr帮我们做了。

class A
{	
public:
	A() {}
};

class HelpPtr
{
public:
	HelpPtr(A* pA) 
	{
		m_pA = pA;
	}
	~HelpPtr() 
	{
		delete m_pA;
	}
private:
	A* m_pA = nullptr;
};
int main()
{
	A* pA = new A();
	HelpPtr ptr(pA);	
	return 0;
}

再看上面的代码,将第一部分代码稍作变形,将指针A保存到HelpPtr中,即使没有在main函数中调用delete,妈妈再也不担心内存泄漏了。
以下内容只会介绍C++11之后的智能指针,C++11之前的std::auto_ptr已经被摒弃,另外例如非官方大神实现的scoped_ptr智能指针,这里都不做介绍。

独享智能指针(unique_ptr)

std::unique_ptr是一个独享型的智能指针,它不允许其他的智能指针共享其内部的指针。
独享智能指针帮助我们管理释放指针,它不允许其他智能指针共享其内部的指针。为实现这种效果,我们应该把独享类的拷贝构造函数和赋值函数都隐藏起来。当然,为了这个独享类能将其他类包装成智能指针,必须使用模板类的方式来实现。

//unique.h
#pragma once

template<typename  T>
class UniquePtr
{
public:
	UniquePtr(T* p = nullptr) :m_p(p){}
	~UniquePtr()
	{
		if (m_p) delete m_p;
	}

	T* operator->()
	{
		return this->m_p;
	}

	T& operator*()
	{
		return *(this->m_p);
	}

	T* get()
	{
		return this->m_p;
	}

protected:
	UniquePtr(const UniquePtr<T>& ptr) {}
	UniquePtr<T>& operator=(const UniquePtr<T>& ptr) {}

private:
	T* m_p = nullptr;
};
#pragma once

#include <iostream>
#include "unique.h"
using namespace std;

class A
{
public:
	A() {}
};

int main()
{
	A* pA = new A();
	UniquePtr<A> APtr(pA);

	UniquePtr<A> A2Ptr = APtr; //报错
	return 0;
}

上面的代码编译时,会报如下错误。因为我们实现的是独享指针,那么就不能将智能指针赋值给其他的智能指针,这其实就是scope_ptr的实现。
在这里插入图片描述
C++11后形成了std::unique_ptr智能指针,基本和scope_ptr相同,不过它们之间也有一些细微差别。就是unique_ptr可以对右值进行转移,对右值转移这是啥意思呢?说明白了就是提供了一种特殊方法可以将unique_ptr赋值给另一个unique_ptr,被转移后的unique_ptr也就不能再处理之前管理的指针了。

//unique.h
#pragma once

template<typename  T>
class UniquePtr
{
public:
	UniquePtr(T* p = nullptr) :m_p(p){}
	~UniquePtr()
	{
		if (m_p) delete m_p;
	}

	T* operator->()
	{
		return this->m_p;
	}

	T& operator*()
	{
		return *(this->m_p);
	}

	T* get()
	{
		return this->m_p;
	}

	UniquePtr(UniquePtr<T>&& ptr) noexcept 
		: m_p(ptr.m_p) 
	{
		ptr.m_p = nullptr;
	}

	UniquePtr& operator=(UniquePtr<T>&& ptr) noexcept
	{
		if (this != &ptr)
		{
			m_p = ptr.m_p;
			ptr.m_p = nullptr;
		}
		return *this;
	}

protected:
	UniquePtr(const UniquePtr<T>& ptr) {}
	UniquePtr<T>& operator=(const UniquePtr<T>& ptr) {}

private:
	T* m_p = nullptr;
};
#pragma once

#include <iostream>
#include "unique.h"
using namespace std;

class A
{
public:
	A() {}
};

int main()
{
	A* pA = new A();
	UniquePtr<A> APtr(pA);

	//UniquePtr<A> A2Ptr = APtr; //报错
	UniquePtr<A> A2Ptr = std::move(APtr);
	return 0;
}

上面main函数中的第四行调用的是拷贝构造函数,由于该函数不是public属性,所以调用该行时会失败。而第五行会调用移动构造函数,因为我们已经实现了移动构造函数,所以该行可以编译成功。在运行时,当APtr移动给A2Ptr 后,APtr也就失去了对原指针的控制权,这在代码中也有体现就是将 ptr.m_p设置为nullptr了。

上面就是C++11标准中的unique_ptr的实现,这样一分析下来也是蛮简单的对吧。

共享智能指针(shared_ptr)

虽然unique_ptr已经很好用了,但有时候我们还是需要多个智能指针管理同一块堆内存空间。有个不错的解决方案—引用计数法。其基本原理是当有多个智能指针指对同一块堆空间进行管理时,每增加一个智能指针引用计数就增1,每减少一个智能指针引用计数就减少。当引用计数减为0时,就将管理的堆空间释放掉。
看文字可能比较抽象,直接上代码:

//shared.h
#pragma once

template<typename  T>
class SharedPtr
{
public:
	SharedPtr(T* p = nullptr) :m_p(p), m_pRefCnt(new int(1)) {}

	SharedPtr(const SharedPtr<T>& ptr)
	{
		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(*m_pRefCnt);
	}

	SharedPtr<T>& operator=(const SharedPtr<T>& ptr) 
	{
		if (this != &ptr)
		{
			releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉

			m_p = ptr.m_p;
			m_pRefCnt = ptr.m_pRefCnt;
			++(*m_pRefCnt);
		}

		return *this;
	}

	~SharedPtr()
	{
		releaseRef();
	}

	T* operator->()
	{
		return this->m_p;
	}

	T& operator*()
	{
		return *(this->m_p);
	}

	T* get()
	{
		return this->m_p;
	}

	int refCnt()
	{
		return *m_pRefCnt;
	}

private:
	void releaseRef()
	{
		if (--(*m_pRefCnt) == 0) 
		{
			delete m_p;
			delete m_pRefCnt;
		}
	}

private:
	T* m_p = nullptr;
	int* m_pRefCnt = nullptr;
};
#pragma once

#include <iostream>
#include "shared.h"
using namespace std;

class A
{
public:
	A() {}
};

int main()
{
	A* pA = new A();
	SharedPtr<A> APtr(pA);

	SharedPtr<A> A2Ptr = APtr; //不会报错
	return 0;
}

从结果中我们可以看到创建APtr时引用计数为 1,将APtr赋值给A2Ptr时引用计算都为2。当main程序结束后首先释放A2Ptr ,其引用计数都减1。再释放APtr,引用计数减为0,当引用计数为0时,释放堆空间。这样的智能指针还是非常棒的,我们再也不怕内存泄漏了!

可惜的是,我们高兴的有点早,当指针循环指向的时候,还是会出现内存泄漏的情况。例如:

struct Node
{
    SharedPtr<Node> _prev;
    SharedPtr<Node> _next;

    ~Node()
    {
        std::cout << "delete :" <<this<< std::endl;
    }
};

SharedPtr<Node> ptrCur(new(Node));
SharedPtr<Node> ptrNext(new(Node));
ptrCur->_next = ptrNext;
ptrNext->_prev = ptrCur;

这里大家可以自行分析一下释放顺序,有助于大家进一步理解引用计数,这里就不分析内存泄漏的原因了。

弱智能指针(weak_ptr)

为了解决这个bug,C++11提供了一个辅助指针类型weak_ptr。实际上weak_ptr不能单独称为一个智能指针,它必须与shared_ptr一起使用,起到辅助share_ptr的作用。我们来看看它是如何解决上述问题的吧。
在之前的shared.h上修改,直接上代码:

#pragma once

struct Counter
{
	int sRef = 0; //存放SharedPtr引用计数
	int wRef = 0; //存放WeakPtr的引用计数
};

template<typename T>
class WeakPtr;

template<typename  T>
class SharedPtr
{
public:
	SharedPtr(T* p = nullptr) :m_p(p), m_pRefCnt(new Counter())
	{
		if (p != nullptr)
		{
			m_pRefCnt->sRef = 1;
		}
	}

	SharedPtr(const SharedPtr<T>& ptr)
	{
		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(m_pRefCnt->sRef);
	}
	
	SharedPtr(const WeakPtr<T>& ptr)
	{
		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(m_pRefCnt->sRef);
	}

	SharedPtr<T>& operator=(const SharedPtr<T>& ptr) 
	{
		if (this != &ptr)
		{
			releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉

			m_p = ptr.m_p;
			m_pRefCnt = ptr.m_pRefCnt;
			++(m_pRefCnt->sRef);
		}

		return *this;
	}

	~SharedPtr()
	{
		releaseRef();
	}

	T* operator->()
	{
		return this->m_p;
	}

	T& operator*()
	{
		return *(this->m_p);
	}

	T* get()
	{
		return this->m_p;
	}

	int refCnt()
	{
		return m_pRefCnt->sRef;
	}

private:
	void releaseRef()
	{
		if (--(m_pRefCnt->sRef) == 0) 
		{
			delete m_p;
			if(m_pRefCnt->wRef == 0)
			{
				delete m_pRefCnt;
				m_pRefCnt = nullptr;
			}
		}
	}

private:
	T* m_p = nullptr;
	Counter* m_pRefCnt = nullptr;

	friend class WeakPtr<T>;
};

template<typename T>
class WeakPtr
{
public:
	WeakPtr(){}

	WeakPtr(const WeakPtr<T>& ptr)
	{
		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(m_pRefCnt->wRef);
	}

	WeakPtr(const SharedPtr<T>& ptr)
	{
		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(m_pRefCnt->wRef);
	}

	WeakPtr<T>& operator=(const WeakPtr<T>& ptr)
	{
		if (this != &ptr)
		{
			releaseRef(); //接受另外一个智能指针时,应该将自身的先释放掉

			m_p = ptr.m_p;
			m_pRefCnt = ptr.m_pRefCnt;
			++(m_pRefCnt->wRef);
		}

		return *this;
	}

	WeakPtr& operator=(const SharedPtr<T>& ptr)
	{
		releaseRef();

		m_p = ptr.m_p;
		m_pRefCnt = ptr.m_pRefCnt;
		++(m_pRefCnt->wRef);

		return *this;
	}

	~WeakPtr()
	{
		releaseRef();
	}

	SharedPtr<T> lock()
	{
		return SharedPtr<T>(*this);
	}

	bool expried()
	{
		if (m_pRefCnt && m_pRefCnt->sRef > 0)
			return false;

		return true;
	}

private:
	void releaseRef()
	{
		if (m_pRefCnt)
		{
			--(m_pRefCnt->sRef);
			
			if (m_pRefCnt->wRef < 1 && m_pRefCnt->sRef < 1)
			{
				m_pRefCnt = nullptr;
			}
		}
	}

private:
	T* m_p = nullptr;
	Counter* m_pRefCnt = nullptr;
};
#pragma once

#include <iostream>
#include "shared.h"
using namespace std;

struct Node
{
	WeakPtr<Node> _prev;
	WeakPtr<Node> _next;

	~Node()
	{
		std::cout << "delete :" << this << std::endl;
	}
};

int main()
{
	SharedPtr<Node> cur(new(Node));
	SharedPtr<Node> next(new(Node));
	cur->_next = next;
	next->_prev = cur;

	return 0;
}

以上就是WeakPtr的实现以及SharedPtr的改造,从中我们可以看到,SharedPtr与我们之前的SharedPtr区别并不是很大,主要做了以下三点修改:
1、以前只有一个计数器,然在变成了两个,一个是SharedPtr本身的计数,另一个是WeakPtr的计数;
2、是增加了一个参数为WeakPtr引用的拷贝构造函数;
3、m_ptr和m_pRefCnt的释放都是在SharedPtr中完成的,WeakPtr不做具体的释放工作。

WeakPtr是新增加的弱指针,它是配合SharedPtr使用的,自己并不能单独使用。WeakPtr也包含m_ptr和m_pRefCnt两个成员,但它更多是是引用,对它们没有创建和释放权。另外在WeakPtr中会对Counter对象的wRef操作,也就是说多个WeakPtr指向同一个堆空间时,它仅操作Counter中的wRef。
因此,对于我们之前的SharedPtr形成环后导致的内存泄漏可以通过WeakPtr对其进行改造,这样内存泄漏的问题就迎刃而解了。

同样的,上面main函数的释放过程,大家自己分析。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值