• C++进阶---智能指针


    RAII

    RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。这样不需要显式地释放资源,同时,采用这种方式,对象所需的资源在其生命期内始终保持有效


    示例
    在除0操作抛出异常的时候:如果在当前函数体内申请过资源,就需要进行一次try-catch以下并在catch内加上delete语句释放资源

    double div()
    {
    	double a, b;
    	cin >> a >> b;
    	if (b == 0)
    		throw invalid_argument("0");
    	return a / b;
    }
    void f1()
    {
    	int* p = new int;
    	try
    	{
    		cout << div() << endl;
    	}
    	catch (...)
    	{
    		delete p;
    		cout << p << endl;
    		throw;
    	}
    
    	delete p;
    	cout << p << endl;
    }
    
    int main()
    {
    	try
    	{
    		f1();
    	}
    	catch (exception& e)
    	{
    		cout << e.what() << endl;
    	}
    	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
    • 34
    • 35
    • 36
    • 37
    • 38

    但如果将这个申请的资源交给智能指针管理,在生命周期结束后会自动调用它的析构函数释放资源达到效果:

    double div()
    {
    	double a, b;
    	cin >> a >> b;
    	if (b == 0)
    		throw invalid_argument("0");
    	return a / b;
    }
    void f1()
    {
    	SmartPtr<int> sp(new int);
    	*sp = 10;
    	cout << *sp << endl;
    	cout << div() << endl;
    }
    
    int main()
    {
    	try
    	{
    		f1();
    	}
    	catch (exception& e)
    	{
    		cout << e.what() << endl;
    	}
    	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

    在这里插入图片描述
    smartptr:

    template<class T>
    class SmartPtr
    {
    public:
    	// 1、RAII
    	// 2、重载operator* 和 operator->  用起来像指针一样
    	SmartPtr(T* ptr)
    		:_ptr(ptr)
    	{}
        ~SmartPtr()
    	{
    		delete _ptr;
    		cout <<"~smart"<< _ptr << endl;
    	}
    	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

    C++98的auto_ptr

    上面的智能指针并没有解决所有问题,当尝试拷贝这个指针的时候,会出错(两次析构)
    在这里插入图片描述
    在这里插入图片描述


    C++98版本的库中就提供了auto_ptr的智能指针

    auto_ptr的使用

    auto_ptr通过管理权转移的方式防止了两次析构的问题,图中可见当转移后原来的dt被置空了(delete空不会出错)

    auto_ptr的问题:当对象拷贝或者赋值后,前面的对象悬空,当尝试修改dt时出错,这是一种不好的设计
    这里是引用

    auto_ptr模拟实现

    auto_ptr的实现原理:管理权转移

    template<class T>
    	class auto_ptr
    	{
    	public:
    		// 1、RAII
    		// 2、重载operator* 和 operator->  用起来像指针一样
    		auto_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    		// sp2(sp1) 管理权转移
    		auto_ptr(auto_ptr<T>& sp)
    			:_ptr(sp._ptr)
    		{
    			sp._ptr = nullptr;
    		}
    		// ap2 = ap3;
    		auto_ptr<T>& operator=(auto_ptr<T>& ap)
    		{
    			if (this != &ap)//防止自己赋给自己
    			{
    				delete _ptr;
    				_ptr = ap._ptr;
    				ap._ptr = nullptr;
    			}
    			return *this;//ap2
    		}
    		~auto_ptr()
    		{
    			if (_ptr)
    			{
    				delete _ptr;
    				cout << _ptr << endl;
    			}
    		}
    		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
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    在这里插入图片描述

    C++11的unique_ptr, shared_ptr, weak_ptr

    C++11的unique_ptr, shared_ptr, weak_ptr 是由 boost库中的scope_ptr, shared_ptr, weak_ptr分别演变过来的

    unique_ptr

    unique_ptr的使用

    针对不需要拷贝的场景:直接防拷贝
    在这里插入图片描述

    unique_ptr的模拟实现

    模拟实现:

    template<class T>
    	class unique_ptr
    	{
    	public:
    		// 1、RAII
    		// 2、重载operator* 和 operator->  用起来像指针一样
    		unique_ptr(T* ptr)
    			:_ptr(ptr)
    		{}
    
    		// 防拷贝
    		unique_ptr(const unique_ptr<T>& up) = delete;
    		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
    
    		~unique_ptr()
    		{
    			if (_ptr)
    			{
    				delete _ptr;
    				cout << _ptr << endl;
    			}
    		}
    
    		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
    • 33
    • 34
    • 35

    注意:

    // C++98防拷贝的方式:只声明不实现+声明成私有
    unique_ptr(unique_ptr<T> const &);
    unique_ptr & operator=(unique_ptr<T> const &);
    // C++11防拷贝的方式:delete
    unique_ptr(unique_ptr<T> const &) = delete;
    unique_ptr & operator=(unique_ptr<T> const &) = delete;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述

    shared_ptr

    shared_ptr的使用

    成员函数:
    这里是引用

    • get:返回所管理资源的地址(指针),存储的指针指向 shared_ptr 对象解引用的对象,通常与其拥有的指针相同
    • use_count:返回所有的引用计数(包括自己)
    • unique:判断该shared_ptr是否是唯一管理资源的智能指针 (空指针非unique)
    • 重载*和->可以像指针一样使用

    shared_ptr的模拟实现

    采用计数的方式,拷贝一个count++,析构时–count,count等于0时表示是最后一个管理对象,就释放资源
    注意:

    • 多个智能指针对象管理一个资源
      count不能为单个对象私有的,会出现不释放资源的问题,将其定义为static变量貌似可行(不能全局,全局变量有很多缺陷)
      在这里插入图片描述
    • 多个智能指针管理多个资源:
      明显static变量就不行,会出现count为负的情况,这里应该使用int* 存储计数
      在这里插入图片描述

    operator=:对于将管理一个资源的sp对象赋值给另一个对象,应该注意讨论:
    例如sp1=sp4:注意点看注释

    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
    	//if (*this != sp) //未重载 != 不能比较
        //if (this != &sp) //原生指针可以直接比较
    	if (_ptr != sp._ptr) //原生指针可以直接比较
    	{
    		if (--(*_pCount) == 0)//对于任意条件--(*_pCount)均要执行
    	    //若因为sp1改为管理sp4所管理的资源,若sp1是最后一个管理其原来管理的资源的sp对象,应释放该资源与计数
    		{
    			delete _pCount;
    			delete _ptr;
    		}
            //若剩余其他sp对象管理sp1资源,进行转移管理
    		_ptr = sp._ptr;
    		_pCount = sp._pCount;
    		++(*_pCount);
    	}	
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    现代写法:不需要考虑边界情况,同时可以减少不必要的操作(如赋值给自己,先-- 再++)

    shared_ptr<T>& operator=(shared_ptr<T> sp)
    //传值创建一个中间对象sp sp和sp4管理的是同一块资源(=左边this,()里为右边)
    {//例如之前sp4的_pCount为2 sp1的_pCount为1,此时sp的_pCount就为3 (拷贝构造的逻辑)
    	swap(_ptr, sp._ptr); //sp和sp1交换
    	swap(_pCount, sp._pCount); //sp和sp1交换
    	return *this;
    }//sp出作用域自己销毁
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    总代码:

    template<class T>
    class shared_ptr
    {
    public:
    	shared_ptr(T* ptr)
    		:_ptr(ptr)
    		, _pCount(new int(1))
    	{}
    	shared_ptr(shared_ptr<T>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    	{
    		++(*_pCount);
    	}
    	shared_ptr<T>& operator=(shared_ptr<T> sp)
    	{
    		swap(_ptr, sp._ptr);
    		swap(_pCount, sp._pCount);
    
    		return *this;
    	}
    	~shared_ptr()
    	{
    		if (--(*_pCount) == 0 && _ptr)
    		{
    			delete _ptr;
    			delete _pCount;
    			cout << _ptr << endl;
    		}
    	}
    	T& operator*()
    	{
    		return *_ptr;
    	}
    	T* operator->()
    	{
    		return _ptr;
    	}
    private:
    	T* _ptr;
    	int* _pCount;
    };
    
    • 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

    线程安全

    注意:

    • 访问资源的线程安全智能指针管不了,属于使用者管理
      智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
    • 智能指针对象拷贝析构的过程中引用计数的线程安全需要保证 (管理同一资源的应该是一把锁,拷贝构造时赋值)
      智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作是线程安全的

    在这里插入图片描述
    注意这里的ref(p)右值问题,参考:C++11 多线程(std::thread)详解


    shared_ptr私有成员增加一个mutex* _pMtx;
    在模拟shared_ptr时单独将原子操作分离出来

    • 增加计数:
    void add_ref()
    {
    	_pMtx->lock();
    	++(*_pCount);
    	_pMtx->unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 减少计数:(删除器见下面)
      注意:需要释放锁资源 (计数为0时,释放资源时)
    void release_ref()
    {
    	bool flag = false;
    	_pMtx->lock();
    	if (--(*_pCount) == 0 && _ptr)
    	{
    		D del;
    		del(_ptr); // 使用删除器释放即可
    		//delete _ptr;
    		delete _pCount;
    		flag = true;
    		cout << "释放资源:" << _ptr << endl;
    	}
    	_pMtx->unlock();
    	if (flag == true)
    	{
    		delete _pMtx;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    更改构造函数和拷贝构造的初始化列表 和 析构函数:
    注意:std库中shared_ptr构造函数加了explicit关键字,防止传参的原生指针隐式类型转换为智能指针

    explicit shared_ptr(T* ptr = nullptr)
    	:_ptr(ptr)
    	, _pCount(new int(1))
    	, _pMtx(new mutex)
    {}
    
    shared_ptr(shared_ptr<T, D>& sp)
    	:_ptr(sp._ptr)
    	, _pCount(sp._pCount)
    	, _pMtx(sp._pMtx)
    {
    	add_ref();
    }
    
    ~shared_ptr()
    {
    	release_ref();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    循环引用

    例如有一个类:包含一个该类的_prev智能指针一个该类的_next智能指针,主函数创建两个该类的智能指针,同时让他们链接起来:
    在这里插入图片描述
    看到并没有释放资源


    在这里插入图片描述
    当出作用域时b先销毁,countb减为1,a再销毁,counta减为1,均未调用析构函数,而a要销毁的条件是_next销毁,_next指向b,b要销毁的条件是_prev销毁,而_prev指向a,如此形成死循环,需要weak_ptr解决,见下

    lambda+shared_ptr=内存泄漏

    参考:Lambda + shared_ptr<> = memory leak
    参考:lambda和shared_ptr的内存泄漏

    定制删除器(MARK??lambda模板参数????)

    std库中shared_ptr在构造函数的时候可以传入删除器
    在这里插入图片描述
    可以这样使用:在这里插入图片描述

    模拟实现

    • std的框架设计底层用一个类专门管理资源计数和释放,所以它可以再构造函数传参,把删除器类型传递给专门管理资源的这个类,但我们是一体化的,只能test::shared_ptr给删除器,析构函数才能拿到删除器
    • shared_ptr更改:定义一个默认删除器,作为D缺省值
    template<class T>
    struct DefaultDel
    {
    	void operator()(T* ptr)
    	{
    		delete ptr;
    	}
    };
    
    template<class T, class D = DefaultDel<T>>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 拷贝构造与operator=的参数返回值更改:
    shared_ptr(shared_ptr<T, D>& sp)
    shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
    
    • 1
    • 2
    • release_ref内部更改为删除器删除:
    D del;
    del(_ptr); // 使用删除器释放即可
    
    • 1
    • 2

    使用:
    在这里插入图片描述

    weak_ptr

    weak_ptr的使用

    为解决循环引用问题引入了weak_ptr

    • weak_ptr的一个重要用途是通过lock获得this指针的shared_ptr,使对象自己能够生产shared_ptr来管理自己
    • weak_ptr只可以从一个shared_ptr或另一个 weak_ptr 对象来构造, 它的构造和析构不会引起引用记数的增加或减少

    在这里插入图片描述

    • 重载了=号:注意右操作数可为shared_ptr
    • reset:对象变为空,就像默认构造的一样
    • use_count:同其他智能指针,注意这里不增加引用计数
    • expired: 检查是否为空或其所管理的资源是否还有其他shared_ptr在管理
      注意:expired的指针在locked时充当空的weak_ptr 对象,因此不能再用于恢复拥有的shared_ptr
      weak_ptr不改变其所共享的shared_ptr实例的引用计数,那就可能存在weak_ptr指向的对象被释放掉这种情况,这时就不能使用weak_ptr直接访问对象
    • lock:如果未被expired,则返回带有由 weak_ptr 对象保留的信息的 shared_ptr

    注意

    • weak_ptr并未重载*和->,不能像指针一样使用
    • weak_ptr在使用前需要检查合法性(调用expired()函数判断是否被destroy)

    可以参考weak_ptr基本用法以及怎么解决循环引用
    如何解决循环引用问题:以循环引用上图为例只需要将a或b的任意一个成员变量改为weak_ptr
    在这里插入图片描述

    weak_ptr的简单模拟

    注意:weak_ptr不参与管理资源,是用来弥补shared_ptr的缺陷
    先将weak_ptr设为shared_ptr的友元

    template<class T>
    class weak_ptr
    {
    public:
    	weak_ptr()
    		:_ptr(nullptr)
    	{}
    
    	weak_ptr(shared_ptr<T>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    	{}
    
    	weak_ptr(weak_ptr<T>& sp)
    		:_ptr(sp._ptr)
    		, _pCount(sp._pCount)
    	{}
    
    	weak_ptr<T>& operator=(shared_ptr<T>& sp)
    	{
    		_ptr = sp._ptr;
    		_pCount = sp._pCount;
    
    		return *this;
    	}
    
    	weak_ptr<T>& operator=(weak_ptr<T>& sp)
    	{
    		_ptr = sp._ptr;
    		_pCount = sp._pCount;
    
    		return *this;
    	}
    private:
    	T* _ptr;
    	int* _pCount;
    };
    
    • 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

  • 相关阅读:
    使用maven命令打包依赖
    易航网址引导系统 v1.9 源码:去除弹窗功能的易航网址引导页管理系统
    几种经典排序算法
    线性代数_同济第七版
    【车辆计数】基于matlab GUI背景差分法道路行驶多车辆检测【含Matlab源码 1911期】
    PyTorch深度学习实战(18)——目标检测基础
    Git工具使用全解
    数据库系统原理与应用教程(045)—— MySQL 查询(七):聚合函数
    dm关键字提示报错
    C#实现HTTP访问类HttpHelper
  • 原文地址:https://blog.csdn.net/qq_41420788/article/details/126445984