【C++】:智能指针详解

版权声明:本文为博主原创文章,欢迎转载,转载请声明出处! https://blog.csdn.net/hansionz/article/details/85997180

智能指针详解

前言:本篇博客介绍C++中的四个智能指针auto_ptr、shared_ptr、weak_ptr、 unique_ptr。其中,auto_ptr存在很大的缺陷,被C++11弃用。

我们为什么要使用智能指针呢?

C++的内存管理是让很多事都需要程序员自己去处理,例如:当我们写一个new语句时,就一定要存在对应的delete语句去释放资源,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针实质上就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源

了解RAII: RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句 柄、网络连接、互斥量等等)的简单技术。在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。RAII实质上是把管理一份资源的责任托管给了一个对象。

//RAII,利用对象的生命周期管理资源
//下面的类只体现了智能,没有体现指针,要体现指针,必须重载*和->
template<class T>
class SmartPtr
{
public:
	//构造时获取获取资源
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	//析构时释放资源
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

使用RAII管理资源有一些好处:

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

什么是智能指针?
首先要声明的是RAII并不是智能指针,RAII只是利用对象声明周期管理资源的一种技术,要为只能指针,必须要让该对象具备指针的特性,也就是可以解引用和->

模拟实现一个简单的智能指针:

template<class T>
class SmartPtr
{
public:
	//构造时获取获取资源
	SmartPtr(T* ptr = nullptr)
		:_ptr(ptr)
	{}
	//析构时释放资源
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	//像指针一样
	//重载operator*和operator->
	T& operator*()
	{
		return *_ptr;
	}
	//当该指针指向结构体时
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};
//test
struct Date
{
	int _year;
	int _month;
	int _date;
};
int main()
{
	SmartPtr<Date> ptr(new Date);
	//相当于ptr.operator->()->_year = 2018;
	//这里为了提高可读性省略了一个->
	ptr->_year = 2019;
	ptr->_month = 1;
	ptr->_date = 1;
	return 0;
}

auto_ptr指针: C++98版本的库中就提供了auto_ptr的智能指针。但是由于其存在一些不能接受的bug,所以现在已经弃用,但是我们仍然应该学习它的思想。auto_ptr的原理是独占所有权,管理权转移,任何时候只能存在一个对象管理资源。

根据库中的代码模拟实现一份简单的auto_ptr学习原理:

//模拟实现auto_ptr
template<class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr)
		:_ptr(ptr)
	{}
	~AutoPtr()
	{
		if (_ptr)
			delete _ptr;
	}
	//拷贝赋值(管理权转移,永远只能保持一个对象去管理资源)
	//公司明确规定不能使用auto_ptr
	//缺陷:使得第一个对象悬空
	AutoPtr(AutoPtr<T>& ap)
	{
		_ptr = ap._ptr;
		ap._ptr = nullptr;
	}
	//重载赋值
	AutoPtr& operator=(AutoPtr<T>& sp)
	{
		//防止自己给自己拷贝
		if (this != &sp)
		{
			//先释放本对像管理的资源
			if (_ptr)
			{
				delete _ptr;
			}
			//赋值
			_ptr = sp._ptr;
			sp._ptr = nullptr;
		}
		//支持连续赋值
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

从上述代码可以看出auto_ptr最大的缺陷是当拷贝对象和赋值时,旧的对象已经悬空,不能对其进行任何操作,否则出现异常。它们在拷贝和赋值时必须要释放掉旧对象管理的资源,因为如果不释放就会是一种浅拷贝,两个指针同时指向一个资源,在释放的时候会释放两次。

unique_ptr: 为了解决auto_ptr的缺陷,C++11标准库引入了新的unique_ptr。unique_ptr智能指针的实现原理简单并且粗暴,它的实现原理就是防止拷贝和赋值。

下面模拟实现一个简单的unique_ptr:

template<class T>
class UniquePtr
{
public:
	UniquePtr(T* ptr)
		:_ptr(ptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}
	//重载*
	T& operator*()
	{
		return *_ptr;
	}
	//重载->
	T* operator->()
	{
		return _ptr;
	}
private:
	//防拷贝和赋值
	//C++98写法,声明为私有只声明不实现
	UniquePtr(UniquePtr<T>& const up);
	UniquePtr& operator=(UniquePtr<T>& const up);
	//C++11写法,delete
	UniquePtr(UniquePtr<T>& const up) = delete;
	UniquePtr& operator=(UniquePtr<T>& const up) = delete;
private:
	T* _ptr;
};

shared_ptr指针: 为了解决unique_ptr不能拷贝的缺陷,C++11标准库又引入了新的支持拷贝和赋值的智能指针shared_ptr。shared_ptr的原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

根据库中的代码模拟实现一份简单的shared_ptr:

template<class T>
class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		:_ptr(ptr)
		, _count(new int(1))
		, _lock(new mutex())
	{
		if (ptr == nullptr)
			*_count = 0;
	}
	//利用互斥量实现原子性的++操作
	int AddRefCount()
	{
		_lock->lock();

		++(*_count);

		_lock->unlock();

		return *_count;
	}
	//实现原子性的--操作
	int SubRefCount()
	{
		_lock->lock();

		--(*_count);

		_lock->unlock();

		return *_count;
	}
	~SharedPtr()
	{
		Release();
	}
	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _count(sp._count)
		, _lock(sp._lock)
	{
		if (_ptr)
			AddRefCount();
	}
	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		//避免自己给自己赋值
		if (_ptr != sp._ptr)
		{
			//释放当前对象管理的资源
			Release();
			_ptr = sp._ptr;
			_count = sp._count;
			AddRefCount();
		}
		//支持连续赋值
		return *this;
	}
	//像指针一样
	//重载operator*
	T& operator*()
	{
		return *_ptr;
	}
	//重载->
	T* operator->()
	{
		return _ptr;
	}
	//返回引用计数
	int UseCount()
	{
		return *_count;
	}
private:
	void Release()
	{
		if (_ptr && SubRefCount() == 0)
		{
			delete _ptr;
			delete _count;
			delete _lock;
		}
	}
private:
	T* _ptr;//维护资源的指针
	int* _count;//引用计数必须是在栈上,这样才可以保证拷贝或赋值的多个对象使用一个公共的引用计数
	            //也不能为静态的变量,静态的对象是所有对象共享
	mutex* _lock;//互斥锁,保护引用计数,因为++和--不是一个原子性的操作
};

shared_ptr的线程安全问题:

智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这 个操作不是原子的,引用计数原来是1++了两次,可能还是2,这样引用计数就错了。会导致资源未释放或者多次释放的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说unique_ptr引用计数的操作是线程安全的。 但是智能指针管理的对象存放在上,两个线程中同时去访问,会导致线程安全问题。

void Test(SharedPtr<Date>& sp, size_t n)
{
	cout << sp.Get() << endl;
	for (size_t i = 0; i < n; i++)
	{
		//智能指针拷贝会++计数
		//智能指针析构会--计数,是线程安全的
		SharedPtr<Date> copy(sp);

		//智能指针访问管理的资源,不是线程安全的。
		//这些值两个线程++了2n次,但是最终看到的结果并一定是加了2n
		copy->_year++;
		copy->_month++;
		copy->_date++;
	}
}
int main()
{
	
	SharedPtr<Date> sp(new Date);
	sp->_year = 2019;
	sp->_month = 1;
	sp->_date = 1;
	cout << sp.Get() << endl;

	const size_t n = 2;    
	thread t1(Test, sp, n);
	thread t2(Test, sp, n);

	t1.join();    
	t2.join();
	
	cout << sp->_year << endl;
	cout << sp->_month << endl;
	cout << sp->_date << endl;

	return 0;
}

总结:

  • shared_ptr本身引用计数是线程安全的
  • shared_ptr管理的资源不是线程安全的

shared_ptr的循环引用问题:

struct Node
{
	int _data;
	shared_ptr<Node> _prev;
	shared_ptr<Node> _next;

	~Node(){ cout << "~Node()" << endl; }
};
int main()
{
	shared_ptr<Node> node1(new Node);
	shared_ptr<Node> node2(new Node);
	//ues_count == 1
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	node1->_next = node2;
	node2->_prev = node1;

	//ues_count == 2
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}
  • node1和node2两个智能指针对象指向两个节点,引用计数变成1,不需要手动delete
  • node1_next指向node2node2_prev指向node1,它们的引用计数变成2
  • node1node2析构,引用计数减到1,但是node1->_next还指向node2节点,node2->_prev还指向node1。
  • _next析构时,node1释放资源,当_prev析构时。node2释放资源。但是_next属于node1的成员,node1释放了,_next才会析构,而node1_prev管理,_prev属于node2 成员,所以这就叫循环引用,谁也不会释放。

解决shared_ptr循环引用问题: 在引用计数的场景下,把节点中的_prev_next改成weak_ptr就可以了 。原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加 node1和node2的引用计数,所以不会造成循环引用的问题。

struct Node
{
	int _data;
	weak_ptr<Node> _prev;
	weak_ptr<Node> _next;

	~Node(){ cout << "~Node()" << endl; }
};
int main()
{
	shared_ptr<Node> node1(new Node);
	shared_ptr<Node> node2(new Node);
	//ues_count == 1
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;

	node1->_next = node2;
	node2->_prev = node1;

	//ues_count == 1
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

代码运行结果:
在这里插入图片描述

如果不是new出来的空间,而是malloc出来的空间,可以通过shared_ptr设计的仿函数删除器来释放:

template<class T>
//仿函数:重载(),使得对象能够像函数一样使用
//仿函数删除器
struct FreeFunc
{
	void operator()(T* ptr)
	{
		cout << "free:" << ptr << endl;
		free(ptr);
	}
};
template<class T>
struct DeleteFunc
{
	void operator()(T* ptr)
	{
		cout << "delete:" << ptr << endl;
		delete(ptr);
	}
};
int main()
{
	FreeFunc<int> fc;
	shared_ptr<int> sp1((int*)malloc(4), fc);

	DeleteFunc<int> dc;
	shared_ptr<int> sp2(new int(4), dc);
	return 0;
}

代码运行结果:
在这里插入图片描述

RAII可以设计守卫锁,防止异常安全导致的死锁问题:

template<class Mutex>
class LockGuard
{
public:
  LockGuard(Mutex& mutex)
    :_mutex(mutex)
  {
    _mutex.lock();
  }
  ~LockGuard()
  {
    _mutex.unlock();
  }
private:
  LockGuard(const LockGuard<Mutex>&) = delete; 
  Mutex& _mutex;//必须要加引用,否则锁住的不是同一个互斥量对象
};
mutex _mtx;
static int n = 0;
void Func()
{
  for(size_t i = 0; i< 10000; i++)
  {
    LockGuard<mutex> lock(_mtx);
    n++;//保护n
  }
}
int main()
{ 
  int begin = clock();

  thread t1(Func);
  thread t2(Func);
  t1.join();
  t2.join();

  int end = clock();

  cout << n << endl;//20000
  cout << "time:" <<  end-begin << endl;
  return 0;
}

利用对象的生命周期来控制互斥锁,防止在其他异常的情况下没有释放锁的情况。

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/85997180