C++之智能指针

智能指针解决的问题
  1. malloc 出来的空间,没有进行释放,出现内存泄漏,在C++中这个问题极其严重
  2. 异常安全问题。如果在malloc和free之间抛异常,程序没有执行完毕就退出也会造成内存泄漏,这种问题引发的叫做异常安全问题。
智能指针的使用及原理

原理:智能指针是基于RAII思想,利用对象生命周期来管理和释放资源(内存,文件句柄,互斥量等)。

我们来解释一下这句话:
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。

优点:

  • 不用显示的释放资源
  • 保证了在对象生命周期内所需的资源始终有效
    怎么来使用呢?举个栗子:
template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
int main()
{
	int* tmp = (int*)malloc(sizeof(int));
	SmartPtr<int> sp(tmp);  //给tmp找了个对象,一直管到他 狗带
	//dosomething
	return 0;
}

但是这个还不算是智能指针。
既然是指针,那么就必须可以支持指针的操作。指针可以解引用,也可以通过->去
访问所指空间中的内容如,因此,我们还得重载 * 和 ->.
我们完善一下上面的操作:

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	T& operator *()
	{
		return *_ptr;
	}
	T* operator ->()
	{
		return _ptr;
	}
	~SmartPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
struct Date
{
	int year;
	int month;
	int day;
};
int main()
{
	SmartPtr<int> s(new int);
	*s = 666;
	cout << *s << endl;
	SmartPtr<Date> sp(new Date);
	sp->year = 2019;
	sp->month = 1;
	sp->day = 29;
	cout << sp->year << "." << sp->month << "." << sp->day << endl;
	return 0;
}

在这里插入图片描述

常见的智能指针类型
  • auto_ptr
  • unique_ptr
  • shared_ptr
详解各种智能指针
一、auto_ptr(C++98) 智能指针的败笔

关于auto_ptr的介绍我就不说了,感兴趣请移步auto_ptr官方文档

auto_ptr思想就是将管理权转移。

有了上面的核心概念我们来模拟实现一下auto_ptr:

template <class T>
class AutoPtr
{
public:
	AutoPtr(T* ptr=nullptr)
	{}

	AutoPtr(AutoPtr<T>& ap) //将原来的资源转移到新的对象,跟自己脱离关系
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	AutoPtr<T>& operator=(AutoPtr<T> ap)
	{
		//确认是不是自己给自己赋值
		if (this != &ap)
		{
			//释放当前对象资源
			if (_ptr)
				delete _ptr;
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~AutoPtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};

struct Date
{
	int year;
	int month;
	int day;
};
int main()
{
	AutoPtr<Date> ap(new Date);
	//这里在拷贝的时候将ap资源置空,导致再次访问资源的时候指针被悬空,内存访问异常,这就是auto_ptr失败的原因
	AutoPtr<Date> copy(ap);
	//copy->month = 1;
	ap->year = 2019;
	cout << copy->month << ap->year << endl;
	return 0;
}

在这里插入图片描述
可以看出来,这个程序是运行不了的,这也是智能指针设计之初的一个败笔,一定不要去用这个,这是个反例。

二、 unique_ptr(C++11) auto_ptr的升级版,比较靠谱的指针

设计原理 : 直接防止拷贝和赋值,简单粗暴
具体操作看代码:

class UniquePtr
{
public:
	UniquePtr(T* ptr=nullptr)
	{}
	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	//C++98防拷贝方式,将重载赋值运算符和拷贝构造设置为私有
	UniquePtr& operator =(UniquePtr<T>& up);
	UniquePtr(UniquePtr<T>const& up);
	
	//C++11防拷贝方式
	UniquePtr& operator =(UniquePtr<T>& up)=delete;
	UniquePtr(UniquePtr<T>const& up)=delete;
private:
	T* _ptr;
};
三、shared_ptr 更靠谱并且能够拷贝的方法

原理: 利用引用计数的方法实现多个对象之间共享数据。
思想:

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

看看代码:

template <class T>
class SharedPtr
{
public:
	SharedPtr(T* ptr=nullptr)
		:_ptr(ptr)
		,_pCount(new int(1))
		,_pMutex(new mutex)
	{
		if (_ptr == nullptr)
		{
			*_pCount = 0;
		}
	}
	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _pCount(sp._pCount)
		, _pMutex(sp._pMutex)
	{ 
		//如果是一个空指针对象,则不用加引用计数,否则就要将 pCount++
		if (_ptr)
			AddCount();
	}
	SharedPtr<T>&operator=(const SharedPtr<T>& sp)
	{
		if (_ptr != sp._ptr)
		{
			//释放旧空间
			Release();

			_ptr = sp._ptr;
			_pCount = sp._pCount;
			_pMutex = sp._pMutex;
			if (_ptr)
				AddCount();
		}
		return *this;
	}
	int UseCount() 
	{ 
		return *_pCount; 
	}
	int AddCount()
	{
		_pMutex->lock();
		++(*_pCount);
		_pMutex->unlock();
		return *_pCount;
	}
	int SubCount()
	{
		_pMutex->lock();
		--(*_pCount);
		_pMutex->unlock();
		return *_pCount;
	}
	void Release()
	{
		if (_ptr && 0 == SubCount())
		{
			delete _ptr;
			delete _pCount;
			delete _pMutex;
		}
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	~SharedPtr()
	{
		Release();
	}
private:
	T* _ptr;
	int* _pCount; //引用计数
	mutex* _pMutex; //互斥锁
};
struct Date
{
	int year;
	int month;
	int day;
};
int main()
{
	SharedPtr<int> sp1(new int(10));
	SharedPtr<int> sp2(sp1);
	*sp2 = 20;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	SharedPtr<int> sp3(new int(10));
	sp2 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;
	sp1 = sp3;
	cout << sp1.UseCount() << endl;
	cout << sp2.UseCount() << endl;
	cout << sp3.UseCount() << endl;
	return 0;
}

结果如下:
在这里插入图片描述

shared_ptr中的线程安全问题

注意: shared_ptr本身是线程安全的,除了下面两种情况

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

举个栗子:

struct Date
{
	int _year;
	int _month;
	int _day;
};

void SharePtrFunc(shared_ptr<Date>& sp, size_t n)
{
	cout << sp.get() << endl;
	for (size_t i = 0; i < n; ++i)
	{
		// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。
		shared_ptr<Date> copy(sp);
		// 这里智能指针访问管理的资源,不是线程安全的。所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n
		copy->_year++;
		copy->_month++;
		copy->_day++;
	}
}
int main()
{
	shared_ptr<Date> p(new Date);
	cout << p.get() << endl;
	const size_t n = 100;
	thread t1(SharePtrFunc, p, n);
	thread t2(SharePtrFunc, p, n);
	t1.join();
	t2.join();
	cout << p->_year << endl;
	cout << p->_month << endl;
	cout << p->_day << endl;
	return 0;
}
智能指针引发的循环引用问题

先看段代码:

struct ListNode
{
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> node1(new ListNode);
	shared_ptr<ListNode> node2(new ListNode);
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	node1->_next = node2;
	node2->_prev = node1;
	cout << node1.use_count() << endl;
	cout << node2.use_count() << endl;
	return 0;
}

在这里插入图片描述
很明显,这个代码是有问题的,我们的ListNode本应该被析构,结果却没有,这是为什么呢?我们来分析一下:

  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就可以了。
struct ListNode
{
	weak_ptr<ListNode> _prev;
	weak_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

原因:

  • 析构函数不释放资源,不遵循 RAII,
  • 没有增加引用计数,
  • 专门辅助解决shared_ptr循环引用的缺陷

好了,到这里C++智能指针部分就基本完成了,有什么疑问欢迎留言。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值