智能指针基本原理,简单实现,常见问题

发布时间 2023-04-12 16:04:01作者: 石中火本火

基本概念

  • 智能指针是一个模板;
  • shared_ptr允许多个指针指向同一个对象,unique指针则独占指向的对象;

基本使用

  • shared_ptr<T> ptr; //默认初始化保存着一个空指针

  • shared_ptr<int> ptr = make_shared<int>(42);

  • 拷贝与赋值,会有一个引用计数

    引用计数增加的情况:

    • 拷贝初始化:shared_ptr<T>q(p);
    • 参数传递及函数返回值:void function(shared_ptr<T> ptr);因为这也是一种拷贝

基本实现

  • 两个基本成员ptr与ref_count,即指针与引用计数,关于引用计数是一个数值还是一个稍复杂的类,要看库的具体实现,此处为了简便使用一个数值来统计引用计数;

    ptr
    ref_count
  • 需实现函数:

    • 显式初始化构造函数
    • 拷贝构造函数
    • 析构函数
#include<iostream>
#include<mutex>
#include<thread>
using namespace std;

template<class T>
class Shared_Ptr{
public:
	Shared_Ptr(T* ptr = nullptr) // 默认构造函数
		:_pPtr(ptr)
		, _pRefCount(new int(1))
		, _pMutex(new mutex)
	{}
	~Shared_Ptr()  // 定制析构函数
	{
		Release();
	}
	Shared_Ptr(const Shared_Ptr<T>& sp)  // 拷贝构造函数,增加引用计数
		:_pPtr(sp._pPtr)
		, _pRefCount(sp._pRefCount)
		, _pMutex(sp._pMutex)
	{
		AddRefCount();
	}
	Shared_Ptr<T>& operator=(const Shared_Ptr<T>& sp) // 重载赋值运算符
	{
		//if (this != &sp)
		if (_pPtr != sp._pPtr)
		{
			// 释放管理的旧资源
			Release();
			// 共享管理新对象的资源,并增加引用计数
			_pPtr = sp._pPtr;
			_pRefCount = sp._pRefCount;
			_pMutex = sp._pMutex;
			AddRefCount();
		}
		return *this;
	}
	T& operator*(){
		return *_pPtr;
	}
	T* operator->(){
		return _pPtr;
	}
	int UseCount() { return *_pRefCount; }
	T* Get() { return _pPtr; }
	void AddRefCount()
	{
		_pMutex->lock();
		++(*_pRefCount);
		_pMutex->unlock();
	}
private:
	void Release()
	{
		bool deleteflag = false;
		_pMutex->lock();
		if (--(*_pRefCount) == 0)
		{
			delete _pRefCount;
			delete _pPtr;
			deleteflag = true;
		}
		_pMutex->unlock();
		if (deleteflag == true)
			delete _pMutex;
	}
private:
	int *_pRefCount; // 计数器
	T* _pPtr;        // 指针成员
	mutex* _pMutex;  
};

线程安全问题

  • 引用计数是多个智能指针对象共享,若智能指针处于不同的线程内,则线程并行操作有可能引起技术混乱或指针空悬问题;
  • 计数混乱:为了解决计数混乱问题,可以加互斥锁在计数变量上,这样每次只会有一个线程执行变量的加减操作;即在实现中引用计数的操作是线程安全的
  • 指针空悬:对指针的操作不是线程安全的。如对于多线程中的两个智能指针a,b,若在某线程1中想要执行赋值操作a=b,分两步执行,1)先执行指针的复制,2)再执行引用计数的复制并加1,这两步操作不是原子的. 如果执行完第一步后,转到了线程2执行b = (new ClassA),即对于智能指针b来说,他被赋予了新值,原计数减一,由于线程一中的增加引用计数操作还未来得及实施,所以现在引用计数变成了0,原指针被释放。如果此时再回到线程1,a的引用计数指针将指向新的b的引用计数,这里就产生了错误,并且a的指针空悬,产生安全问题。
  • 具体图示可见link:https://blog.csdn.net/liang19890820/article/details/120465794

循环引用

可以假设一个双向链表Node的结构体,其中的next与prev都设置为智能指针,然后创建两个智能指针node1, ndoe2指向两个新new的Node,那么这时候两者的引用计数都是1.此时再分别设置node1->next = node2; node2->next = node1,两者的引用计数都变为2,若此时程序结束,则两node引用值都变为1,无法析构,。想要node1析构得先析构node2,反之亦然,这就导致了循环。

  • 解决方法:shared_ptr改为weak_ptr,弱指针不会增加引用计数,就不会循环引用了