C++ — 智能指针二

shared_ptr

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

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
//实现了引用计数
//引用++,析构--,到1时释放
template<class T>
class shared_ptr
{
public:
    shared_ptr(T* ptr)
        :_ptr(ptr)
            ,_pcount(new int(1))    //1为缺省值
        {}
    shared_ptr(const shared_ptr<T>& sp) 
        :_ptr(sp._ptr)
            ,_pcount(sp._pcount)
    {
        (*pcount)++;
    }
    shared_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        //if(this != &sp)    //sp1与sp2管理同一块空间时,sp1 = sp2时,会--后又++
        if(_ptr != sp._ptr)    //同一块空间使用同一个引用计数
        {
            if(--(*_pcount ) == 0)
            {
                 //开始使用新空间,需要释放旧空间的引用计数,但是原来的空间是公用的,所以需要判断
                delete _ptr;
                delete _pcount;   
            }
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            *(_pcount)++;
        }
        return *this;
    }
    ~shared_ptr()
    {
        if(--(*_pcount ) == 0)
        {
            delete _ptr;    
            delete _pcount;
            _pcount = nullptr;
        }
    }
private:
    T* _ptr;
    static int* _pcount;    //防止出现多个引用计数
    //多个空间,_pcount只有一个,所以每使用一块新空间就就开辟一块空间存放
    //_pcount并不保证线程安全,但是互斥锁是有缺陷的,互斥锁适合力度大的,自旋锁适合力度小的
};

shared_ptr的线程安全问题

shared_ptr的线程安全分为两方面

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的.
  2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
//线程安全问题演示
namespace my
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr)
			:_ptr(ptr)
			, _pcount(new int(1))    //1为缺省值
			, _pmutex(new std::mutex)
		{}
		shared_ptr(const shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			AddRefCount();
		}
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if(this != &sp)    //sp1与sp2管理同一块空间时,sp1 = sp2时,会--后又++
			if (_ptr != sp._ptr)    //同一块空间使用同一个引用计数
			{
				Release();
				_ptr = sp._ptr;
				_pcount = sp._pcount;
				_pmutex = sp._pmutex;
				AddRefCount()
			}
			return *this;
		}
		~shared_ptr()
		{
			Release();
		}
		void AddRefCount()	//在需要count++的位置调用
		{
			// 加锁或者使用加1的原子操作
			_pmutex->lock();
			++(*_pRefCount);
			_pmutex->unlock();
		}
		void Release()	//完成   释放
		{
			bool deleteflag = false;
			_pMutex.lock();
            //这里最好使用RAII设计的互斥锁更加安全
            //unique_lock<mutex> lock(m_mtx);
			if (--(*_pRefCount) == 0)
			{
				//开始使用新空间,需要释放旧空间的引用计数,但是原来的空间是公用的,所以需要判断
				delete _ptr;
				delete _pcount;
				//delete _pmutex;	//不能提前释放锁,因为还没有解锁
				_pcount = nullptr;
				_ptr = nullptr;
				
				deleteflag = true;
			}
			_pMutex.unlock();
			if (deleteflag == true)
			{
				delete _pMutex;
				_pmutex = nullptr;
			}
				
		}
		T* Get_ptr()
		{
			return _ptr;
		}
		int Get_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		static int* _pcount;    //防止出现多个引用计数
		//多个空间,_pcount只有一个,所以每使用一块新空间就就开辟一块空间存放
		//_pcount并不保证线程安全,但是互斥锁是有缺陷的,互斥锁适合力度大的,自旋锁适合力度小的
		std::mutex* _pmutex; // 互斥锁,管理同一个锁	
	};
	struct Date
	{
		int _year;
		int _month;
		int _day;
		~Date()
		{}
	};
};

void SharePtrFunc(my::shared_ptr<my::Date>& sp, size_t n)
{
	std::cout << sp.Get_count() << std::endl;
	for (size_t i = 0; i < n; ++i)	//使用p这个智能指针拷贝
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		my::shared_ptr<my::Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
        //这里sharet_ptr是安全的,但是Date类中的资源却不是安全的,可能多个线程同时取使用
		/*copy->_year++;    
		copy->_month++;
		copy->_day++;*/
	}
}
//正常调用Date的析构函数则无错
int main()
{
	my::shared_ptr<my::Date> p(new my::Date);	//创建Date类并用智能指针p管理
	std::cout << p.Get_count() << std::endl;
	const size_t n = 1000;	//数值=10时正确调用,等于1000则未正常调用,说明多线程是有概率问题的,需要考虑线程安全的问题
	//加入线程安全
	std::thread t1(SharePtrFunc, p, n);	//头文件
	std::thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();

	return 0;
}

shared_ptr本身是线程安全的,但是管理的资源并不是安全的,
成员为类对象时,需要使用->去访问

shared_ptr的循环引用

struct ListNode
{
	int _data;
	std::shared_ptr<ListNode> _prev;
	std::shared_ptr<ListNode> _next;
	~ListNode(){ std::cout << "~ListNode()" << std::endl; }
};
int main()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);
	std::cout << node1.use_count() << std::endl;
	std::cout << node2.use_count() << std::endl;
	//两行同时应用则无法正确释放
	node1->_next = node2;
	node2->_prev = node1;
	std::cout << node1.use_count() << std::endl;    //获取shared_ptr的引用计数
	std::cout << node2.use_count() << std::endl;
	return 0;
}
//循环引用分析:
//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的引用计数。
struct ListNode
{
	int _data;
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;
	~ListNode(){ std::cout << "~ListNode()" << std::endl; 
};
//weak_ptr是专门为解决shared_ptr的循环引用而解决的,不会增加引用计数,但是可以将shared_ptr的值赋给weak_ptr.
  1. 智能指针不一定能管理malloc出来的空间,因为智能指针管理的这一块空间并没有初始化,这块空间中如果没有指针,释放的时候是去调用析构函数delete,本质上是使用free,不需要考虑指针的问题。
    但是如果这块空间中有指针(如string),这个指针也指向一块空间,使用malloc而没有调用构造函数,指针为随机值,结束后调用析构函数delete时会释放这个指针,而这个指针是随机值,则出错。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值