• 【C++】智能指针:auto_ptr、unique_ptr、share_ptr、weak_ptr(技术介绍 + 代码实现)(待更新)



    0. 概述

    智能指针,智能在哪儿?

    • 使用了模板类,建立的是 智能指针对象,自动调用智能指针类型的构造和析构函数。也就是说,对于动态开辟的空间如果用智能指针保存,就不需要手动释放啦,极大程度降低了内存泄漏的风险。
      • 这样利用对象生命周期进行程序资源控制的技术就是 RAII。
    • *-> 的重载,使 智能指针对象 具有指针的行为能力,能让用户像使用指针一样的使用。

    RAII 的介绍

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

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

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

    四个智能指针的特点:

    我把四个智能指针的特点介绍在前面,你若还有什么细节问题再去具体的栏目下翻找吧~

    • auto_ptr:管理权转移,通过拷贝构造函数和赋值重载函数来实现。
      • 原对象拷贝给新对象的时候,原对象就会被设置为nullptr,此时就只有新对象指向一块资源空间。
      • 会出现指针悬空问题。
    • unique_ptr:禁用拷贝构造和赋值构造
      • unique_ptr(unique_ptr&) = delete;
      • operator=(unique_ptr&) = delete;
    • share_ptr:引用计数
      • 计数的对象在堆上,所有线程都能访问,因此需要锁保证其安全性
      • 会出现循环引用的问题
    • weak_ptr:弱关联性
      • weak_ptr 类的对象它可以指向 shared_ptr,并且不会改变 shared_ptr 的引用计数

    1. auto_ptr(C++98)

    核心功能:管理权转移

    管理权转移的同时也会导致 原指针悬空,容易造成野指针问题,不推荐使用。

    🐎核心功能的简单实现

    namespace ttang
    {
    	template<class T>
    	class auto_ptr
    	{
    	public:
    		auto_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    
    		~auto_ptr()
    		{
    			if (_ptr)
    			{
    				cout << "delete:" << _ptr << endl;
    				delete _ptr;
    			}
    		}
    		// 无力吐槽的神拷贝...
    		// 管理权转移:导致的是原来的指针悬空,很多公司明令禁止使用 auto_ptr
    		auto_ptr(auto_ptr<T>& ap)
    			:_ptr(ap._ptr)
    		{
    			ap._ptr = nullptr;
    		}
    		
    		T* operator=(auto_ptr<T>& ap)
    		{
    			T* tmp = ap._ptr;
    			ap._ptr = nullptr;
    			return tmp;
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    	private:
    		T* _ptr;
    	};
    --------------------------------------------------
    	void test_auto()
    	{
    		auto_ptr<int> ap1(new int(1));
    		auto_ptr<int> ap2(ap1);
    
    		*ap1 = 1; // err...管理权转移以后导致ap1悬空,不能访问
    		*ap2 = 1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    2. unique_ptr(C++11)

    核心功能:防拷贝(= delete 声明拷贝构造和复制重载)

    unique_ptr 的指针,简单粗暴,是防了拷贝,不过也只解决了不需要拷贝的场景。
    (ps:从 boost 里面吸收来的)
    (pps:需要拷贝的场景就需要使用到接下来会介绍的 shared_ptr 和 weak_ptr 了)

    🐎核心功能的简单实现

    namespace ttang
    {
    	template<class T>
    	class unique_ptr
    	{
    	private:
    		T* _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;
    		}
    
    		// C++11思路:设置不许再实现了,语法直接支持的(不需要私有了)
    		unique_ptr(const unique_ptr<T>& up) = delete;
    		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;	// 严格来说赋值也封了更好一点
    
    		// C++98思路:只声明不实现,但是用的人可能会在外面强行定义,所以再加一条,声明为私有
    	//private:
    		// unique_ptr(const unique_ptr& up);
    		// unique_ptr& operator=(const unique_ptr& up);
    	};
    --------------------------------------------------
    	void test_unique()
    	{
    		unique_ptr<int> up1(new int(1));
    		unique_ptr<int> up2(up1);	// err...
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    3. shared_ptr(C++11)

    核心功能:引用计数

    之前在一开始的概述部分介绍了两个智能指针为什么智能的原因,走到了 shared_ptr,我们的智能指针就真的更神了,他甚至还引申出 智能指针三大件 的说法:

    • RAII
    • 想指针一样使用
    • 可以拷贝(浅拷贝!!)

    shared_ptr 允许拷贝,意在允许多个智能指针可以指向同一块资源,并且能够保证共享的资源只会被释放一次,程序不会多次析构而崩溃。为了保证在最后一个智能指针使用完毕才释放,C++11 使用了引用计数的技术。

    在具体对 shared_ptr 实现之前,对于这里的 引用计数,可以稍微探讨一下:

    1)如果要使用引用计数,设置一个静态变量 count 行不行呢?不行,因为静态变量属于所有对象。而 每实例化一个对象都可能多有个资源,每个资源应该配对一个引用计数

    2)如果是多个线程去调用引用计数,还需要保证其线程安全,那就加个锁吧。

    3)计数加锁后,shared_ptr 本身就会是线程安全的,但是他生成的对象不是线程安全的。

    🐎核心功能的简单实现(手撕 shared_ptr 版本)🔺

    namespace ttang
    {
    	template<class T>
    	class shared_ptr{
    	private:
    		T* _ptr;	// 用指针,一个资源的多个指针要看见并修改这一个count
    		int* _pcount;
    	public:
    		// 【构造】
    		shared_ptr(T* ptr = NULL)
    			: _ptr(ptr)
    			, _pcount(new int(1)) 
    		{}
    		// 【拷贝构造】
    		shared_ptr(const shared_ptr& s)
    			: _ptr(s.ptr)
    			, _pcount(s._pcount)
    		{
    			*(_pcount)++;
    		}
    		// 【赋值重载】
    		/*
    		* 【1】正常赋值:
    		* sp1 = sp4;					被赋值sp1原来有资源,的肯定要把原来的资源--,sp4要++
    		* 【2】自己给自己赋值:
    		* sp1 = sp1;				自己给自己	
    		* sp1 = sp2;(管理一个资源)	也是自己给自己,这时候 if(&sp != this) 就不得行了哦,防不了这一种
    		*/
    		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    		{
    			// 检查不是自己赋给自己
    			if(this != &sp)
    			{ 
    				// 赋值之前查一下自己的_pcount, 
    				if(--(*_pcount) == 0)
    				{
    					delete _ptr;
    					delete _pcount;
    				}
    				_ptr = s._ptr;
    				_pcount = s._pcount;
    				*(_pcount)++;
    			}
    			return *this;
    		}
    		
    		// 模拟指针行为,解引用 return 数据内容
    		T& operator*()
    		{
    			return *_ptr;
    		}
    		// 模拟指针行为,箭头指向 return 指针自己
    		T* operator->()
    		{
    			return _ptr;
    		}
    		//【析构】
    		~shared_ptr()
    		{ 
    			--(*_pcount);
    			if(*_pcount == 0)
    			{
    				delete _ptr;
    				delete _pcount;
    				_ptr = nullptr;
    				_pcount = nullptr;
    			}
    		}
    		// 还可以扩展,看下面的代码示例
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    🐎核心功能的扩展实现

    在上面的基础上:

    • 加锁,保证计数的线程安全
    • 定制删除器,供用户可以自定义传入删除器
    namespace ttang
    {
    	template<class T>
    	class shared_ptr
    	{
    	private:
    		T* _ptr;
    		int* _pcount;								
    		mutex* _pmtx;								// 锁也得是指针,因为是多个指针指向同一把锁
    		function<void(T*)> _del = [](T* ptr) {		// 解决对 deletor 的保存问题,需要一个缺省的!!
    			cout << "lambda delete:" << ptr << endl;
    			delete ptr;
    		};
    	public:
    		shared_ptr(T* ptr = nullptr)
    			:_ptr(ptr)
    			, _pcount(new int(1))	// 每个资源都分配一个引用计数count
    			, _pmtx(new mutex)		// 每个资源都有一把锁,保证自己资源计数安全
    		{}
    
    		// 定制删除器(通过仿函数实现的!--是可调用对象,所以我们拿的一个function定义_del)
    		template<class D>
    		shared_ptr(T* ptr, D del)
    			: _ptr(ptr)
    			, _pcount(new int(1))
    			, _pmtx(new mutex)
    			, _del(del)
    		{}
    		
    		~shared_ptr()
    		{
    			Release();
    		}
    
    		void Release()
    		{
    			_pmtx->lock();
    
    			bool deleteFlag = false;
    
    			if (--(*_pcount) == 0)
    			{
    				if (_ptr)
    				{
    					//cout << "delete:" << _ptr << endl;
    					//delete _ptr;
    					_del(_ptr);			// 如果_del 不给缺省的话,这里默认的构造可能会出问题
    				}
    				delete _pcount;
    
    				deleteFlag = true;
    				// delete _pmtx;  锁也要释放的呀,可以下面又要解锁。如何解决?
    			}
    
    			_pmtx->unlock();
    
    			if (deleteFlag)
    			{
    				delete _pmtx;
    			}
    		}
    
    		void AddCount()
    		{
    			_pmtx->lock();
    
    			++(*_pcount);
    
    			_pmtx->unlock();
    		}
    
    		shared_ptr(const shared_ptr<T>& sp)
    			:_ptr(sp._ptr)
    			, _pcount(sp._pcount)
    			, _pmtx(sp._pmtx)
    		{
    			AddCount();
    		}
    
    		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    		{
    			if (_ptr != sp._ptr)
    			{
    				Release();
    
    				_ptr = sp._ptr;
    				_pcount = sp._pcount;
    				_pmtx = sp._pmtx;
    
    				AddCount();
    			}
    
    			return *this;
    		}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    		T* get()
    		{
    			return _ptr;
    		}
    
    		int use_count()
    		{
    			return *_pcount;
    		}
    	};
    	
    	void test_shared()
    	{
    		shared_ptr<int> sp1(new int(1));
    		shared_ptr<int> sp2(sp1);
    		shared_ptr<int> sp3(sp2);
    
    		shared_ptr<int> sp4(new int(10));
    
    		//sp1 = sp4;
    		sp4 = sp1;
    
    		sp1 = sp1;
    		sp1 = sp2;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131

    3.1 shared_ptr 的 多线程问题

    我们说 shared_ptr本身是线程安全的,因为计数是加锁保护的;
    那 shared_ptr 管理的对象是否是线程安全呢?不安全。

    如果需要多线程访问资源,需要程序员手动加锁。

    🌰举例:

    namespace ttang
    {
    	struct Date
    	{
    		int _year = 0;
    		int _month = 0;
    		int _day = 0;
    	};
    
    	void SharePtrFunc(ttang::shared_ptr<Date>& sp, size_t n, mutex& mtx)
    	{
    		//cout << sp.get() << endl;
    		//cout << &sp << endl;
    
    		for (size_t i = 0; i < n; ++i)
    		{
    			// 智能指针拷贝会++计数,析构会--计数,这里是线程安全的。
    			ttang::shared_ptr<Date> copy(sp);
    
    			mtx.lock();
    			sp->_year++;
    			sp->_day++;
    			sp->_month++;
    			mtx.unlock();
    		}
    	}
    
    	void test_shared_safe()
    	{
    		ttang::shared_ptr<Date> p(new Date);
    		cout << p.get() << endl;
    
    		const size_t n = 10000;
    		mutex mtx;
    		thread t1(SharePtrFunc, ref(p), n, ref(mtx));	// 线程中以引用传递对象参数,必须加一个ref(),是一个库函数,否则会认为是传值传参会报错。
    		thread t2(SharePtrFunc, ref(p), n, ref(mtx));	// 13 底下可以检测,19 是直接报错
    		//cout << &p << endl;	
    
    		t1.join();
    		t2.join();
    
    		cout << p.use_count() << endl;
    
    		cout << p->_year << endl;
    		cout << p->_month << endl;
    		cout << p->_day << endl;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    3.2 share_ptr 循环引用的问题

    这里举例一个 List 数据结构

    我们按照传统通常这样去定义一个节点:

    struct ListNode{
    	ListNode* _next;
    	ListNode* _prev;
    	// ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后这样去使用他:

    ListNode* n1 = new ListNode;
    ListNode* n2 = new ListNode;
    
    n1->_next = n2;
    n2->_prev = n1;	
    
    //...	
    如果在这里有异常抛出,后面的代码就不会执行了~
    //...
    
    delete n1;
    delete n2;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    但事实上,这样使用会出现一个问题,如果在 delete 节点前,就抛异常,代码运行逻辑就出去了,会导致没有释放的情况。好说,我们学过了 RAII,可以选择利用对象的生命周期来实现对资源的控制,于是乎可以在使用时将节点定义成 shared_ptr。

    循环引用的产生

    Node 节点定义成 shared_ptr,要完成 Node1->_next = Node2,就同样需要在结构体里把 _next 和 _prev 定义成 shared_ptr。

    那么代码就应该写成这样:

    struct ListNode{
    	ttang::shared_ptr _next;
    	ttang::shared_ptr _prev;
    	// ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然后这样去使用他

    std::shared_ptr<ListNode> n1(new ListNode);	// std 里面只能这样显示的调构造
    std::shared_ptr<ListNode> n2(new ListNode);
    
    cout << n1.use_count() << endl;	//1
    cout << n2.use_count() << endl;	//1
    
    n1->_next = n2;	
    n2->_prev = n1;	
    
    cout << n1.use_count() << endl;	//2
    cout << n2.use_count() << endl;	//2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    向上面这样,shared_ptr 管理的两个节点相互指向,奇怪的事情就出现了:

    在这里插入图片描述

    可以看到,随着程序的结束,两个原本应该随进程周期结束而析构的指针,并没有析构,也就是说出现了未释放、内存泄露的情况。怎么回事呢?

    循环引用的原因分析

    在这里插入图片描述

    • 如图所示:当 Node1_ptr 被建立的时候,其引用就为 1 了,在被 Node2_ptr 指向的时候,引用就变成了 2。同理 Node2_ptr 也一样。
    • 当程序结束的时候两个 ptr 对象都理应调用自己的析构函数,但是其内部的指针互相指向引用计数始终不为 0 无法析构,这导致了两个对象没有真正的被回收。
    • 书面原理:成员的生命周期,取决于对象的生命周期,对象的生命周期结束则成员调用析构函数,成员源于被另一个智能指针管理,无法释放,形成闭环。
    • 这就是循环引用产生的原因。

    针对上述问题的出现,C++11 提供了一个解决方案:


    4. weak_ptr(C++11)

    核心功能:弱连接 ,专门用来解决 shared_ptr 的循环引用问题。

    注意有三:

    • 他不是常规的智能指针,不支持 RAII
    • 支持像指针一样
    • 专门设计出来,辅助解决 shared_ptr 的循环引用问题

    weak_ptr 的智能指针可以指向 shared_pre 的指针指向的资源,而不增加 share_ptr 的引用计数。

    于是定义结点的代码应该修改成这样:

    struct ListNode
    {	
    	std::weak_ptr _next;		
    	std::weak_ptr _prev;
    	int _val;
    	
    	~ListNode()
    	{
    		cout << "~ListNode()" << endl;
    	}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    🐎核心功能的简单实现

    namespace ttang
    {
    	template<class T>
    	class weak_ptr	// 超简单实现,库里肯定不是这样滴
    	{
    	public:
    		weak_ptr()
    			:_ptr(nullptr)
    		{}
    
    		weak_ptr(const shared_ptr<T>& sp)
    			:_ptr(sp.get())
    		{}
    
    		T& operator*()
    		{
    			return *_ptr;
    		}
    
    		T* operator->()
    		{
    			return _ptr;
    		}
    
    		T* get()
    		{
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    	};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    了解:定制删除器

    特殊定制析构方式,不难,看代码吧。
    结合 ttang::shared 里的,构造的时候第二个参数传入可调用对象就行。

    //  定制删除器 -- 可调用对象
    template<class T>
    struct DeleteArray
    {
    	void operator()(T* ptr)
    	{
    		cout << "void operator()(T* ptr)" << endl;
    		delete[] ptr;
    	}
    };
    struct Date
    {
    	int _year = 0;
    	int _month = 0;
    	int _day = 0;
    };
    
    void test_shared_deletor()
    {
    	ttang::shared_ptr<Date> sp0(new Date);
    
    	ttang::shared_ptr<Date> spa1(new Date[10], DeleteArray<Date>());
    	ttang::shared_ptr<Date> spa2(new Date[10], [](Date* ptr) {
    		cout << "lambda delete[]:" << ptr << endl;
    		delete[] ptr;
    		});
    
    	ttang::shared_ptr<FILE> spF3(fopen("Test.cpp", "r"), [](FILE* ptr) {
    		cout << "lambda fclose:" << ptr << endl;
    		fclose(ptr);
    		});
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    🥰如果本文对你有些帮助,欢迎👉 点赞 收藏 关注,你的支持是对作者大大莫大的鼓励!!(✿◡‿◡) 若有差错恳请留言指正~~


  • 相关阅读:
    如何使用Idea打开、导入、运行maven项目
    【系统架构设计】架构核心知识:4 系统可靠性分析与设计
    买学生台灯应该怎么选择?学生护眼台灯推荐
    jmeter压测GraphQL接口(两种方式)
    数字秒表VHDL启动暂停清零,源码和视频
    Linux线程
    HLS + ffmpeg 实现动态码流视频服务
    Centos7软件包管理(rpm、yum)
    ubuntu 20.04 ROS 环境下 使用 velodyne
    【我的日志】关于我可爱的新同事
  • 原文地址:https://blog.csdn.net/m0_67470729/article/details/134098268