c++智能指针详解

c++智能指针

文章目录

  • c++智能指针
    • 为什么要有智能指针?
    • 智能指针有哪些实现版本
    • auto_ptr 的实现版本1
    • auto_ptr 的实现版本2
      • 两种 auto_ptr 的对比
    • unique_ptr 的实现版本
    • shared_ptr 和 weak_ptr
      • shared_ptr 的简单实现
      • shared_ptr线程安全问题
      • shared_ptr在双向循环链表中引起的循环引用问题
      • weak_ptr帮助shared_ptr解决循环引用
        • shared_ptr 和 weak_ptr 内部关系
        • *weak_ptr 解决循环引用的图解过程

为什么要有智能指针?

c++最令人失望的是没有资源回收功能,资源的回收依赖于显示定义的回收资源的函数。为了实现资源的自动回收,c++引入了一种能够近似自动回收的方法就是智能指针。

智能指针是基于一种叫做 RAII 的思想,(Resource Acquisition Is Initialization) 将资源的回收在定义变量的时候就设置好。智能指针的作用其实就是将资源的管理分配给每一个对象,随着对象的消亡而调用析构函数然后将资源粉碎掉。

这样做的好处是:

1.不需要显示的释放资源

2.采用这种方式,对象所需的资源在其生命周期内始终有效。

智能指针有哪些实现版本

智能指针不应该只是一个对象,它还应该能够干指针能干的事情,比如说 * 解引用
和 -> 箭头,如果有需要的话还必须有 = 号功能。也就是说要对这些功能进行函数重载。

auto_ptr 的实现版本1


	template <class T>
    class AutoPtr
    {
    private:
    	T* _Ptr;
    public:
    	AutoPtr(T *ptr = nullptr) :_Ptr(ptr)
    	{
    	}
    	AutoPtr(const AutoPtr<T> &ap) :_Ptr(ap._Ptr)
    	{
    		ap._Ptr = nullptr;
    	}
    	AutoPtr<T> &operator = (const AutoPtr<T> &ap)
    	{
    		if (this != &ap)
    		{
    			delete this->_Ptr;
    			this->_Ptr = ap._Ptr;
    			ap._Ptr = nullptr;
    		}
    		return *this;
    	}
    	~AutoPtr()
    	{
    		delete _Ptr;
    	}
    	T& operator *()
    	{
    		if (_Ptr == nullptr)
    		{
    			throw ap;
    		}
    		return *_Ptr;
    	}
    	T& operator ->()
    	{
    		if (_Ptr == nullptr)
    		{
    			throw ap;
    		}
    		return _Ptr;
    	}
    };

c++智能指针详解_第1张图片
以上auto_ptr的主要思想是,

1.资源只能被一个指针管理
2.如果使用一个管理了资源的指针去初始化另一个指针的话。那么管理资源的权限将会被转移。
3.转移过后的指针将会被赋予nullptr。也就是被悬空了。

以上存在的问题,如果空指针被错误的解引用会造成程序崩溃。

auto_ptr 的实现版本2

为了使得多个职能指针共同使用同一块空间,在以上的基础上做了一点改变。设立一个owner的bool变量,只有为true的owner才能够释放资源。使用拷贝构造函数或者赋值运算符时,会造成管理权限的转移。

    template <class T>
    class Auto_ptr{
    private:
    	mutable T * _ptr;
    	mutable	bool owner;
    public:
    	Auto_ptr(T *ptr = nullptr) :_ptr(ptr), owner(true)
    	{}
    	Auto_ptr(const Auto_ptr & ap) :_ptr(ap._ptr), owner(true)
    	{
    		if (this != &ap)
    		{
    			ap.owner = false;
    		}
    	}
    	Auto_ptr<T> & operator = (const Auto_ptr & ap)
    	{
    		if (this != &ap)
    		{
    			if (ap)
    			{
    				ap.owner = false;
    				this->owner = true;
    			}
    		}
    	}
    	T & operator *()
    	{
    		if (_ptr)
    		{
    			return *_ptr;
    		}
    	}
    	T & operator ->()
    	{
    		if (_ptr)
    		{
    			return this->_ptr;
    		}
    	}
    	~Auto_ptr()
    	{
    		if (_ptr)
    		{
    			if (owner == true)
    			{
    				delete _ptr;
    			}
    		}
    	}
    };

以上auto_ptr 的主要思想是:

1.多个指针对应一个资源
2.只有一个指针拥有对资源的释放的权利
3.使用拷贝构造函数和赋值运算符会将释放资源的权利进行转移。

两种 auto_ptr 的对比

从效果上来看第二种auto_ptr似乎更好一些。但是在实际运用中第二种auto_ptr有个致命的缺点。

请看下个例子

    int main()
    {
    	Auto_ptr<int> ptr1(new int(1));
    	if (1)
    	{
    		Auto_ptr<int> ptr2(ptr1);
    	}
    	*ptr1 = 10;
    	
    	return 0;
    }

如果在一个作用域里面ptr1调用拷贝构造函数构造ptr2。在作用域的生命周期结束后被自动释放。ptr1变成了野指针

unique_ptr 的实现版本

Unique_ptr是c++11里面新增的一个智能指针版本。它的主旨非常暴力,既然auto_ptr有指针悬空问题。那么就让它不能赋值和拷贝就解决了问题。

    template <class T>
    class Unique_ptr
    {
    private:
    	T * _ptr;
    	Unique_ptr(const Unique_ptr<T> &) = delete;
    	Unique_ptr<T>&operator = (Unique_ptr<T> &ap) = delete;
    public:
    	Unique_ptr(T * ptr = nullptr) :_ptr(ptr)
    	{}
    	~Unique_ptr()
    	{
    		if (_ptr)
    		{
    			delete _ptr;
    		}
    	}
    	T& operator * ()
    	{
    		if (this->_ptr)
    		{
    			return *_ptr;
    		}
    	}
    	T* operator ->()
    	{
    		if (this->_ptr)
    		{
    			return _ptr;
    		}
    	}
    };

阻止赋值和拷贝的方法很简单,将拷贝构造函数和赋值运算符设置为私有的,或者直接加c++ 11 的关键词 = delete。

shared_ptr 和 weak_ptr

shared_ptr 的简单实现

承接string的深浅拷贝的思想。为了解决多个指针管理一份资源的需求。
shared_ptr加入了引用计数的方法。

简单来说就是使用一个指针管理计数空间,假设计数空间里的变量为count,在delete的时候检查计数count = 0 的时候释放资源。

值得注意的是,在进行++count 和 --count 时必须保证原子操作。多线程时候可能会引起误操作。

    #include
    #include
    #include
    using namespace std;
    template <class T>
    
    class Shared_ptr
    {
    private:
    	T * _ptr;
    	int * count;
    	mutex *_pMutex;
    	void Release()
    	{
    		if (_ptr && SubCount() == 0)
    		{
    			delete _ptr;
    			delete count;
    		}
    	}
    	void AddCount()
    	{
    		_pMutex->lock();
    		++(*count);
    		_pMutex->unlock();
    	}
    	int SubCount()
    	{
    		_pMutex->lock();
    		--(*count);
    		_pMutex->unlock();
    		return *count;
    	}
    public:
    	Shared_ptr(T *ptr = nullptr) :_ptr(ptr), count(new int(1)), _pMutex(new mutex)
    	{
    		if (ptr == nullptr)
    		{
    			*count = 0;
    		}
    	}
    	~Shared_ptr()
    	{
    		Release();
    	}
    	Shared_ptr<T>(const Shared_ptr<T> &ap) :
    		_ptr(ap._ptr),
    		count(ap.count),
    		_pMutex(ap._pMutex)
    	{
    		if (_ptr)
    		{
    			AddCount();
    		}
    	}
    	Shared_ptr<T>& operator = (const Shared_ptr<T> &ap)
    	{
    		if (this->_ptr)
    		{
    			if (this->_ptr != ap._ptr)
    			{
    				Release();
    
    				this->_ptr = ap._ptr;
    				this->count = ap.count;
    				this->_pMutex = ap._pMutex;
    				if (_ptr)
    				{
    					AddCount();
    				}
    			}
    		}
    		return *this;
    	}
    	T & operator *()
    	{
    		if (_ptr)
    		{
    			return *_ptr;
    		}
    	}
    	T * operator ->()
    	{
    		if (_ptr)
    		{
    			return ptr;
    		}
    	}
    	int UseCount()
    	{
    		return *count;
    	}
    	T* Get()
    	{
    		return _ptr;
    	}
    };
    int main()
    {
    	Shared_ptr<int> sp1(new int(10));
    	Shared_ptr<int> sp2(sp1);
    	*sp2 = 10;
    	cout << sp1.UseCount() << endl;
    	cout << sp2.UseCount() << endl;
    
    	Shared_ptr<int> sp3(new int(10));
    	sp2 = sp3;
    	cout << sp1.UseCount() << endl;
    	cout << sp2.UseCount() << endl;
    	cout << sp3.UseCount() << endl;
    	
    	sp1 = sp3;
    	cout << sp1.UseCount() << endl;
    	cout << sp2.UseCount() << endl;
    	cout << sp3.UseCount() << endl;
    	system("pause");
    	return 0;
    }

shared_ptr线程安全问题

  1. 智能指针对象引用计数是多个智能指针对象共享的,两个线程中国的职能指针同时使用一个引用计数,造成错乱。最终导致资源没有被正确释放。为了解决这种潜在的问题,需要加一把锁。

  2. 由于智能指针指向的资源存放在堆上面,所以如果两个线程同时访问会导致线程安全问题。

shared_ptr在双向循环链表中引起的循环引用问题

 #include
 #include
 using namespace std;
 class ListNode
 {
 public:
 	int data;
 	shared_ptr<ListNode> pre;
 	shared_ptr<ListNode> next;
 	~ListNode()
 	{
 		cout << "~ListNode()" << endl;
 	}
 };
 int main()
 {
 	shared_ptr<ListNode> sp1(new ListNode);
 	shared_ptr<ListNode> sp2(new ListNode);

 	cout << sp1.use_count() << endl;
 	cout << sp2.use_count() << endl;

 	sp1->next = sp2;
 	sp2->pre = sp1;
 	
 	cout << sp1.use_count() << endl;
 	cout << sp2.use_count() << endl;
 	
 	return 0;
 }

假设建立一个双向循环链表,根据 main()函数的操作,如下来进行说明。
【图1】
c++智能指针详解_第2张图片
shared_ptr sp1是一个智能指针,它的数据成员 ptr 维护了一块链表节点,Ref 维护了一块引用计数。同理可得 sp2。

【图2】
c++智能指针详解_第3张图片
pre 和 next 也是智能指针,next->ptr 和 next->Ref 是空指针,他们不维护资源和引用计数。同理可得 pre。

【图3】
c++智能指针详解_第4张图片
sp1->next = sp2; 该语句使得 sp1->next->ptr 指向了 sp2的资源,并且使得sp2->Ref 所维护的引用计数 +1,此时 sp2 的 count 为 2。
同理可得 sp2->pre = sp1;

【图4】
c++智能指针详解_第5张图片
main()函数生命周期快结束时,调用构造函数对资源释放。以sp1 为例,sp1 指向的引用计数将会由2变为1,由于引用计数不为0,所以不对空间进行释放。同理可得sp2。

【图5】
c++智能指针详解_第6张图片
main()函数的生命周期结束,在堆上出现了内存泄漏。两个节点互指双方,互不释放。造成了循环引用。

weak_ptr帮助shared_ptr解决循环引用

shared_ptr 和 weak_ptr 内部关系

    #include
    #include
    using namespace std;
    class ListNode
    {
    public:
    	int data;
    	weak_ptr<ListNode> pre;
    	weak_ptr<ListNode> next;
    	~ListNode()
    	{
    		cout << "~ListNode()" << endl;
    	}
    };
	
    int main()
    {
    	shared_ptr<ListNode> sp1(new ListNode);
    	shared_ptr<ListNode> sp2(new ListNode);
    	cout << sp1.use_count() << endl;
    	cout << sp2.use_count() << endl;
    	sp1->next = sp2;
    	sp2->pre = sp1;
    	
    	cout << sp1.use_count() << endl;
    	cout << sp2.use_count() << endl;
    	
    	return 0;
    }

改变的位置是 pre 和 next 的类型改为weak_ptr。

通过查看运行中的代码,我们可以的到如下关系。

【图1】
c++智能指针详解_第7张图片
c++智能指针详解_第8张图片
c++智能指针详解_第9张图片
c++智能指针详解_第10张图片
shared_ptr 和 weak_ptr 都继承自同一个基类,这个基类定义了指向资源的指针和引用计数的空间

利用了多态的思想进行设计。

【图2】
c++智能指针详解_第11张图片
c++智能指针详解_第12张图片
这里的引用计数也是一个由基类发展的多种不同类型的引用计数。

**
这里需要注意的是,在基类当中引用计数是两种数据,一种是uses代表着shared_ptr的引用计数,另一种是 weak 代表着 weak_ptr 的引用计数。也就是说 shared_ptr 其实有两个引用变量 uses 和 weak。weak_ptr 不能够自己开辟资源,但是能够管理shared_ptr 中的 weak计数 **

*weak_ptr 解决循环引用的图解过程

基于上面的构成,重新顺理一遍循环引用问题。

【图1】
c++智能指针详解_第13张图片
与之前不同的地方,加入了weak的计数变量。在初始化一个shared_ptr时
,uses 和 weak 会同时++。

【图2】
c++智能指针详解_第14张图片
sp1->next = sp2;sp2->pre = sp1; 经过该语句后weak_ptr类型的sp1->next 和 sp2->pre 指向的weak都加了1。

【图3】
c++智能指针详解_第15张图片
假设先释放sp2,那么sp2->Ref->uses 就要-1,此时sp2->Ref->uses为0,说明要释放sp2->ptr所指向的资源。那么要释放sp2->ptr所指向的资源,就必须释放,sp2->ptr->pre。此时sp2->ptr->pre->sp2->Ref->weak 就要-1。sp2->ptr->pre->ptr 就要与sp1指向的资源断开联系。

【图4】
c++智能指针详解_第16张图片
sp2已被销毁。但是由于 sp2->Ref->uses 和 sp2->Ref->weak 不同时为0,所以它依旧存在。

【图5】
c++智能指针详解_第17张图片
现在释放sp1。 要释放sp1 ,那么sp1->Ref->uses 就要 -1。此时因为uses 为零,所要要释放 sp1->ptr所指向的资源。为了释放资源,就要先释放 sp1->ptr->next 。 此时sp1->ptr->next->Ref->weak 就要-1,
sp1 遗留的引用计数空间就要被销毁了(sp1->Ref->uses == 0 && sp1->Ref->weak == 0)。 最后由于sp1 的销毁 引起了 sp1->Ref->weak 的 -1 操作,此时 sp1->Ref 所指向的引用计数空间也就被销毁了。

【图6】
c++智能指针详解_第18张图片
销毁完毕。

我无敌

你可能感兴趣的:(C++学习)