C++ 智能指针

一、为什么需要智能指针

看如下代码有什么问题:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如果div发生除零错误抛异常的话,此时p1 p2已经开辟好了,抛异常的话会跳转到catch语句而不会执行delete语句了,就会造成内存泄漏。

如果p2申请空间抛异常的话,p1一定是已经开辟好的,最后的delete p1语句就执行不了了,也会造成内存泄漏,虽然我们也可以通过抛异常的方式解决,但是如果开辟的空间比较多的话,代码就会一层套一层,不是很美观

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
		try
		{
			cout << div() << endl;
		}
		catch (...)
		{
			delete p1;
			delete p2;
		}
	}
	catch (...)
	{
		delete p1;
	}
	delete p1;
	delete p2;
}

为了解决上述问题就引入了智能指针

二、智能指针的使用及原理

2.1 RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

接下来我们自己简单模拟实现一下RAII的智能指针

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
            delete _ptr;
   }
    
private:
    T* _ptr;
};
int div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
  throw invalid_argument("除0错误");
  return a / b;
}
void Func()
{
  ShardPtr<int> sp1(new int); //智能指针
  ShardPtr<int> sp2(new int);//智能指针
  cout << div() << endl;
}
int main()
{
    try {
     Func();
   }
    catch(const exception& e)
   {
        cout<<e.what()<<endl;
   }
 return 0;
}

通过智能指针管理p1和p2,此时不论声请p2抛异常还是除零抛异常,跳转到catch语句后,sp1和sp2都会因为出了作用而调用析构函数释放资源,就不会发生内存泄露了,但是上述代码存在一些问题,当两个智能指针指向同一份资源时,就会释放两次资源而崩溃,接下来我们看看库里的智能指针是如何解决这个问题的。

2.2 std:: auto_ptr

auto_ptr是C++98提出来的,它解决这个问题的思路是管理权转移的思想,当另一个智能指针指向资源后,就将上一个智能指针置为空,始终保持只有一个智能指针管理一个资源

// C++98 管理权转移 auto_ptr
namespace zyq
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

但是auto_ptr的设计并不是很好,因为会导致原来的智能指针发生悬空问题,当我们再次使用原来的智能指针可能会发生访问空指针的问题,所以很多公司都禁止使用auto_ptr

int main()
{
 std::auto_ptr<int> sp1(new int);
 std::auto_ptr<int> sp2(sp1); // 管理权转移

 // sp1悬空
 *sp2 = 10;
 cout << *sp2 << endl;
 cout << *sp1 << endl;
 return 0;
}
2.3 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr,它的思想很简单,直接将拷贝构造和赋值禁止掉,不让拷贝

  • C++11库才更新智能指针实现
  • C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
  • C++11将boost库中智能指针精华部分吸收了过来
  • C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		unique_ptr(const unique_ptr<T>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}
2.4 std::shared_ptr

C++11中也提供了更加靠谱的shared_ptr,它与unique_ptr的不同点是他可以拷贝,那他是如何解决原来的问题的呢?它是通过引用计数的方式来解决释放两次资源的问题的,当发生拷贝时,会让这个资源的引用计数加1,当一个智能指针调用析构函数时,首先会让引用计数减1,如果此时引用计数为0的话表示没有智能指针指向这个资源了,就会释放掉这个资源,如果引用计数不为0的话,表示还有智能指针指向这个资源,此时就不会释放掉资源了。

模拟实现:

  因为引用计数涉及++ --的操作,在多线程的情况下并不安全,所以我们需要通过一定的手段让他变得线程安全

方法一:互斥锁

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_numptr(new int(1)),
			_pmtx(new mutex)
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_numptr);
			_pmtx->unlock();
		}

		void release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_numptr) == 0)
			{
				cout << "releasse" << endl;
				delete _ptr;
				delete _numptr;
				flag = true;
			}
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			_pmtx = P._pmtx;
			AddRef();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				_pmtx = P._pmtx;
				AddRef();
			}
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		int* _numptr;
		mutex*  _pmtx;
	};
}

方法二:原子操作

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_numptr(new atomic<int>(1))
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}
		void release()
		{
			if (--(*_numptr) == 0)
			{
				cout << "releasse" << endl;
				delete _ptr;
				delete _numptr;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			(*_numptr)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				(*_numptr)++;
			}
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		atomic<int>* _numptr;
	};

shared_ptr的缺点:循环引用

当发生类似如下的情况时,shared_ptr就会有点问题

struct ListNode
{
   int _data;
   shared_ptr<ListNode> _prev;
   shared_ptr<ListNode> _next;
   ~ListNode(){ cout << "~ListNode()" << endl; }
};

int main()
{
   shared_ptr<ListNode> node1(new ListNode);
   shared_ptr<ListNode> node2(new ListNode);
   cout << node1.use_count() << endl;  //1
   cout << node2.use_count() << endl;  //1
   node1->_next = node2;
   node2->_prev = node1;
   cout << node1.use_count() << endl;  //2
   cout << node2.use_count() << endl;  //2
   return 0;
}
//当程序结束是两个智能指针的引用计数应该都为0,但是这种情况 两个人的引用计数都为1

问题分析:

我们假设node1指针的资源为节点1,node2指针的资源为节点2,当执行到return语句时,node2和node1会首先调用自己析构函数,此时两个节点引用计数--,变为了1,如果想让节点1的引用计数变为0的话就需要让node2的prev析构,那node2的prev什么时候析构呢?node2的prev是节点2管着的,当节点2释放prev就会释放,那节点2什么时候释放呢?当node1的next析构,节点2就会释放,而node1的next只有当节点1释放才会释放,这样节点1与节点2互相影响,就会造成循环引用的问题。为了解决这个问题,C++11也引入了weak_ptr, 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。

2.5 剩余问题

如果不是new出来的对象如何通过智能指针管理呢?由于库的底层释放资源都是delete ptr实现的,如果我们用malloc申请资源怎么办呢?或者我们申请的类型显示定义出了析构函数并且我们申请了多个对象怎么解决呢?

其实shared_ptr设计了一个删除器来解决这个问题,我们可以在创建智能指针时将释放资源的方法也传进去,这样就解决了

// 仿函数的删除器
template<class T>
struct FreeFunc {
     void operator()(T* ptr)
     {
     cout << "free:" << ptr << endl;
     free(ptr);
     }
};
template<class T>
struct DeleteArrayFunc {
     void operator()(T* ptr)
     { 
     cout << "delete[]" << ptr << endl;
     delete[] ptr; 
     }
};
int main()
{
     FreeFunc<int> freeFunc;
     std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
     DeleteArrayFunc<int> deleteArrayFunc;
     std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
    
     std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
     std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
     {
         fclose(p); 
     });
 
     return 0;
}

接下来我们修改一下我们自己模拟实现的shared_ptr

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		template<class D>
		shared_ptr(T* ptr,D del)
			:_ptr(ptr),
			_numptr(new atomic<int>(1)),
			_del(del)
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}
		void release()
		{
			if (--(*_numptr) == 0)
			{
				cout << "release" << endl;
				_del(_ptr);
				delete _numptr;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			(*_numptr)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				(*_numptr)++;
			}
			return *this;
		}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		atomic<int>* _numptr;
		function<void(T*)> _del = [](T* ptr) {delete ptr; }; //不传定制删除器就采用默认的方式
	};
}

int main()
{
	shared_ptr<A> sp1(new A[5], [](A* ptr) {delete[] ptr; });
	shared_ptr<A> sp2((A*)malloc(sizeof(A)*5), [](A* ptr){free( ptr); });
}

  • 29
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张呱呱_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值