• 【C++11】智能指针的使用以及模拟实现(shared_ptr,unique_ptr,auto_ptr,weak_ptr)


    🌏博客主页: 主页
    🔖系列专栏: C++
    ❤️感谢大家点赞👍收藏⭐评论✍️
    😍期待与大家一起进步!



    一、 RAII概念

    RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
    存、文件句柄、网络连接、互斥量等等)的简单技术。
    在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
    对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
    这种做法有两大好处:
    1.不需要显式地释放资源。
    2.采用这种方式,对象所需的资源在其生命期内始终保持有效

    我们下面所说的智能指针都是基于这种思想设计出来的。

    一、auto_ptr

    1.基本使用

    很多公司明确规定不准用auto_ptr,因为其坑点太多了。

    因为其会导致管理权的转移,会导致被拷贝对象悬空,对悬空对象进行操作的时候就会发生报错

    class A
    {
    public:
    	A(int a = 0)
    		:_a(a)
    	{
    		cout << "A(int a = 0)" << endl;
    	}
    
    	~A()
    	{
    		cout << this;
    		cout << " ~A()" << endl;
    	}
    	 
    	int _a;
    };
    int main() {
    
    	auto_ptr<A> ap1(new A(1));
    	 
    
    	// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
    	// 隐患:导致被拷贝对象悬空,访问就会出问题
    	 auto_ptr<A> ap2(ap1);
    
    	 // 崩溃
    	ap1->_a++;
    	ap2->_a++;
    
    
    	return 0;
    }
    
    • 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

    在这里插入图片描述

    2.模拟实现

    template<class T>
    	class auto_ptr {
    	public:
    		auto_ptr(T *ptr)
    			:_ptr(ptr)
    		{}
    		~auto_ptr() {
    			delete _ptr;
    		}
    
    		// 像指针一样使用
    		T& operator*() {
    			return *_ptr;
    		}
    		T* operator->() {
    			return  _ptr;
    		}
    
    		auto_ptr(auto_ptr<T>& ap)
    			:_ptr(ap._ptr)
    		{
    			// 管理权转移
    			//原指针发生悬空
    			ap._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;
    		}
    	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
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    二、unique_ptr

    1.基本使用

    unique_ptr这个智能指针对于auto_ptr的吐糟点解决的就非常简单粗暴,既然你拷贝与赋值都会造成那么多的问题,我直接把你的拷贝和赋值函数给禁用掉,当我们不需要拷贝的场景,建议使用这个智能指针
    在这里插入图片描述
    在这里插入图片描述

    2.模拟实现

    template<class T>
    	class unique_ptr {
    	public:
    		unique_ptr(T*ptr)
    			:_ptr(ptr)
    		{}
    		~unique_ptr() {
    			delete _ptr;
    		 }
    		T& operator*() {
    			return *_ptr;
    		}
    		T* operator->() {
    			return _ptr;
    		}
    		 //直接把拷贝构造函数以及赋值运算符重载函数设置为删除函数
    		unique_ptr(unique_ptr<T>& ap) = delete;
    		unique_ptr<T>operator=(unique_ptr<T>& ap) = delete;
    	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

    三、shared_ptr

    1.基本使用

    shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

    1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共
      享。
    2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减
      一。
    3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
    4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对
      象就成野指针了。

    在这里插入图片描述

    2.引用计数实现

    引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源

    private:
    		T* _ptr;
    		int* _pcount;//引用计数计数器
    		function<void (T*)>_del = [](T* ptr) {delete ptr; };//析构函数包装器
    
    • 1
    • 2
    • 3
    • 4
    ~shared_ptr() {
    			//引用计数的计时器对于指向同一个地址的指针肯定要是公有的
    			//所以我们选择使用对指针解引用的方式改变计数器的值
    			//而且改变了对两个指向同一个地址的指针都有影响
    			if (--(*_pcount) == 0) {
    			//(*_pcount) == 0说明此时已经没有指针指向这个位置,可以释放了
    				cout << "delete:" << endl;
    				_del(_ptr);
    				delete _pcount;
    			}
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    3.析构函数的升级(对于数组)

    当我们遇到数组的情况怎么办?我们析构的话只是析构当前这一个位置,而数组是一连串的,这个时候我们如果还用原来的老方法设计析构函数就会发生内存泄漏的问题

    C++库里面选择自己去实现一个删除方法的类并且作为仿函数传进去
    在这里插入图片描述

    这个时候又衍生一个问题,我删除方法D作为模板参数是在析构函数内的,只在析构函数内有效,而不是在像模板参数T那样在整个类里面都有效,我们该如何解决这个问题呢?

    答案:我们可以选择自己在private里面的再加入一个私有成员_del作为删除方法

    这个时候又有人会有疑惑,我_del该如何既兼容释放一个地址,又兼容释放一串地址呢?我们在外面写的删除函数不一定能传进去适配_del的类型

    答案:这个时候我们之前介绍的function包装器就能派上用场了【C++11】function包装器,bind函数模板使用
    我们可以用一个包装器来兼容多个调用对象的类型,这样我们就能传入自己写的仿函数了。

    template<class D>
    		shared_ptr(T* ptr, D del)
    			: _ptr(ptr),
    			_pcount(new int(1)),
    			_del(del)
    		{}
    		private:
    		T* _ptr;
    		int* _pcount;//引用计数计数器
    		function<void (T*)>_del = [](T* ptr) {delete ptr; };//析构函数包装器
    		//默认为释放一个地址的lambda的方法,我们要删除数组时可以自己写方法传进去,
    		//因为释放地址的函数类型返回值都为void类型
    
    
    
    //当我们析构数组类型的时候可以设计的仿函数
    template<class T>
    struct DeleteArrayFunc {
     void operator()(T* ptr)
     { 
     cout << "delete[]" << ptr << endl;
     delete[] 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

    4.循环引用(坑点)

    在这里插入图片描述
    在这里插入图片描述

    1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动
      delete。
    2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
    3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上
      一个节点。
    4. 也就是说_next析构了,node2就释放了。
    5. 也就是说_prev析构了,node1就释放了。
    6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev
      属于node2成员,所以这就叫循环引用,谁也不会释放。
      造成了类似死循环的场景

    解决方案:
    在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

    原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和
    _prev不会增加node1和node2的引用计数。既然我靠引用计数是否为0来判断是否需要析构,那么我不增加引用计数的个数不就可以了吗?

    5.模拟实现

    template<class T>
    	class shared_ptr {
    	public:
    		shared_ptr(T*ptr=nullptr)
    			:_ptr(ptr),
    			_pcount(new int(1))
    		{}
    		 
    		template<class D>
    		shared_ptr(T* ptr, D del)
    			: _ptr(ptr),
    			_pcount(new int(1)),
    			_del(del)
    		{}
    
    		~shared_ptr() {
    			//引用计数的计时器对于指向同一个地址的指针肯定要是公有的
    			//所以我们选择使用对指针解引用的方式改变计数器的值
    			//而且改变了对两个指向同一个地址的指针都有影响
    			if (--(*_pcount) == 0) {
    				cout << "delete:" << endl;
    				_del(_ptr);
    				delete _pcount;
    			}
    		}
    
    		shared_ptr(const shared_ptr<T>& sp)
    			:_ptr(sp._ptr),
    			_pcount(sp._pcount)
    		{
    			++(*_pcount);
    		}
    
    		shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
    			if (_ptr == sp._ptr) {
    				return *this;
    			}
    			if (--(*_pcount) == 0)
    			{
    				 
    				delete _ptr;
    				delete _pcount;
    			}
    			_ptr = sp._ptr;
    			_pcount = sp._pcount;
    			++(*_pcount);
    			return *this;
    		}
    
    		int use_count()const  {
    			return *_pcount;
    			//获得引用计数的数
    		}
    
    		T* get()const {
    			return _ptr;
    		}
    
    		T& operator*() {
    			return *_ptr;
    		}
    		T* operator->() {
    			return _ptr;
    		}
    
    	private:
    		T* _ptr;
    		int* _pcount;//引用计数计数器
    		function<void (T*)>_del = [](T* ptr) {delete 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
    • 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

    四、weak_ptr

    在这里先事先声明weak_ptr并不是RAII类型的指针,其设计的目的就是为了解决shared_ptr中的循环引用的问题。其不支持像其他智能指针的使用

    template<class T>
    	class weak_ptr {
    	public:
    		weak_ptr()
    			:_ptr(nullptr)
    		{}
    
    		weak_ptr(const shared_ptr<T>& sp)  
    			:_ptr (sp.get())
    		{}
    
    		weak_ptr<T>&operator=(const shared_ptr<T>& sp) {
    			_ptr = sp.get();
    			return *this;
    		}
    		/*shared_ptr node1(new ListNode);
    		shared_ptr node2(new ListNode);
    	//这不设计是为了让shared_ptr类型能够赋值给weak_ptr类型
    	//我们知道node类型为shared_ptr类型,而其prev与next为weak_ptr类型
    		node1->_next = node2;
    		node2->_prev = node1;*/
    
    		T& operator*() {
    			return *_ptr;
    		}
    
    		T* operator->() {
    			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
  • 相关阅读:
    介绍回流与重绘(Reflow & Repaint),以及如何进行优化?
    sd卡上移植filex
    程序设计实践学习笔记
    2018版USBASP烧录器改通用版教程
    DS200DCFBG1BLC IS220PAICH1A 构建人工智能能力背后的紧迫性
    线上多域名实战
    Linux常用命令
    MySQL数据库管理
    基于Promise + XHR 封装myAxios函数
    ..\FreeRTOS\include\FreeRTOS.h(1277): error: #65: expected a “;“出现的这个错误标志解决方法
  • 原文地址:https://blog.csdn.net/m0_74774759/article/details/133892482