C++智能指针

什么是智能指针?为什么要有只能指针?

在C++程序里,使用new关键字开辟的内存必须被手动delete掉,不然就会导致内存的泄漏,但是,当程序非常冗长,你能保证自己每一个手动开辟的内存块都释放了吗?在delete之前,倘若程序因为异常跳转后,你还能保证内存被释放吗?为了解决这种因为遗忘或者异常处理而导致内存泄漏的问题,我们就要用智能指针。

智能指针运用了一种叫做RAII的技术,即利用对象生命周期来控制程序资源的技术,这中技术的思想就是在构建对象时获取我们资源的指针,接着控制对资源的访问使之在对象的生命周期内一直保持有效,在对象析构时释放指针对应的内存块。这样,内存块的管理与释放就完全交给了一个对象,这样做的好处有两点:
1.不需要显示的释放资源
2.对象所需的资源在其声明周期内始终有效

下面模拟最简单的智能指针:

template<class T>
class smartPtr
{
public:
	smartPtr(T* tmp = nullptr)    //构造和析构函数体现了RAII的思想
		:ptr(tmp)
	{}

	~smartPtr()
	{
		if (ptr)
			delete ptr;
	}

	T& operator*()				//operator*和operator->模拟指针的行为
	{
		return *ptr;
	}

	T* operator->()
	{
		return ptr;
	}

private:
	T* ptr;
};

在智能指针的发展历程中,C++不断推出了各种各样的智能指针。但在赋值/拷贝过程,但他们都各有缺陷,直到今日,shared_ptr才算一种成熟的智能指针,下面我们来介绍这些智能指针。

auto_ptr

这个是早期的智能指针,这个智能指针最“挫”

template<class T>
class AutoPtr//模拟实现auto_ptr
{
public:
	AutoPtr(T* tmp = nullptr)
		:_ptr(tmp)
	{}

	~AutoPtr()
	{
		if (_ptr)
			delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}
		
	T* operator->()
	{
		return _ptr;
	}

	AutoPtr(AutoPtr<T>& _tmp)//拷贝构造
		:_ptr(_tmp._ptr)
	{
		_tmp._ptr = nullptr;
	}

	AutoPtr<T>& operator=(AutoPtr<T>& _tmp)//赋值运算符重载,记得要释放原空间
	{
		if (_tmp._ptr != _ptr)
		{
			if (_ptr)
				delete _ptr;
			_ptr = _tmp._ptr;
			_tmp._ptr = nullptr;
		
		}
		return *this;
	}
private:
	T* _ptr;
};

auto_ptr原理:在拷贝 / 赋值过程中,直接剥夺原对象对内存的控制权,转交给新对象,然后再将原对象指针置为nullptr。这种做法也叫管理权转移。他的缺点不言而喻,当我们再次去访问原对象时,程序就会报错,所以auto_ptr可以说实现的不好,很多企业在其库内也是要求不准使用auto_ptr。

unique_ptr

在C++11中推出的unique_ptr,这种指针比上一个指针靠谱一些。

template<class T>
class UniquePtr//模拟实现unique_ptr
{
public:
	UniquePtr(T* tmp = nullptr)
		:_ptr(tmp)
	{}

	~UniquePtr()
	{
		if (_ptr)
			delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	
private:
	UniquePtr(const UniquePtr<T>& tmp) = delete;
	UniquePtr<T>& operator=(const UniquePtr<T>& tmp) = delete;
	T* _ptr;
};

unique_ptr原理:直接吧拷贝构造/赋值函数弄成delete,即将这两个函数定义成已删除的函数,任何试图调用它的行为将产生编译期错误。是C++11标准的内容。C++的做法是将这两个函数设为私有,且只声明不实现。
这种智能指针比起auto_ptr要好上不少,且实现简单。但是遇到要拷贝构造 / 赋值的情景就不能使用了。

shared_ptr

shared_ptr完善了前两种的不足,既不会直接剥夺原对象对内存的控制权,也允许进行拷贝构造和赋值,这都源自于他引入了一个新的标志—引用计数。引用计数记录着有多少块让我们来看看他是怎么实现的:

template<class T>
class SharedPtr//模拟实现shared_ptr
{
public:
	SharedPtr(T* tmp = nullptr)
		:_ptr(tmp)
		,count(new int(1))
	{}

	~SharedPtr()
	{
		if (--(*count) == 0)
		{
			delete _ptr;
			delete count;
		}
			
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	SharedPtr(SharedPtr<T>& tmp)
		:_ptr(tmp._ptr)
		,count(tmp.count)
	{
		(*count)++;
	}


	SharedPtr<T>& operator=(SharedPtr<T>& tmp)
	{
		
		if (_ptr != tmp._ptr)//排除自己给自己赋值的可能
		{
			//先要判断原来的空间是否需要释放
			if (--(*count) == 0)
			{
				delete _ptr;
				delete count;
			}
			_ptr = tmp._ptr;
			count = tmp.count;
			(*count)++;
		}
		return *this;//考虑连等的可能
		
		
	}
private:
	
	T* _ptr;
	int* count;
};

这里先要说说记录引用计数为什么是指针:为了使各个对象都使用同一个变量标记,所以不能直接用整形记录。有人说那行,用个静态的吧。那可不行,用静态的就真正是所有变量都公用一个变量了.比如指针a,b指向内存块A,指针c,d,e指向内存块B,本来a,b的引用计数应该是2,c,d,e的引用计数应该是3,要使用静态的就都变成5了。这里要注意,所以最好的办法就是使用指针。

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

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

shared_ptr引发的线程安全问题

在多进程程序下,多个进程都去访问shared_ptr管理的空间,如果线程是并行的,那么引用计数会可能发生错误!如图:
在这里插入图片描述
编程测试:

#include<iostream>
#include<memory>
#include<thread>
#include<mutex>
using namespace std;

class Date
{
public:
	size_t _year = 0;
	size_t _month = 0;
	size_t _day = 0;
};
template<class T>
class SharedPtr
{
public:
	SharedPtr(T* tmp = nullptr)
		:_ptr(tmp)
		,count(new size_t(1))
	{
		if (tmp == nullptr)
		{
			*count = 0;
		}
	}

	~SharedPtr()
	{
		if (_ptr && (--(*count) == 0))
		{
			delete _ptr;
			delete count;
		}
			
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	SharedPtr(SharedPtr<T>& tmp)
		:_ptr(tmp._ptr)
		,count(tmp.count)
	{
		if (_ptr)
			(*count)++;
	}


	SharedPtr<T>& operator=(SharedPtr<T>& tmp)
	{
		
		if (_ptr != tmp._ptr)//排除自己给自己赋值的可能
		{
			//先要判断原来的空间是否需要释放
			if (--(*count) == 0)
			{
				delete _ptr;
				delete count;
			}
			_ptr = tmp._ptr;
			count = tmp.count;
			if (_ptr)
				(*count)++;
		}
		return *this;//考虑连等的可能
			
	}
	size_t useCount()//返回引用计数的值
	{
		return *count;
	}

	T* get()
	{
		return _ptr;
	}

private:
	
	T* _ptr;
	size_t* count;
	
};

void sharedPtrFunc(SharedPtr<Date>& p,size_t size)//就执行n次拷贝构造
{
	for (size_t i = 0;i < size;i++)
	{
		SharedPtr<Date> copy(p);
	}
	
}
int main()
{
	SharedPtr<Date> p(new Date);

	thread t1(sharedPtrFunc, p, 10000);
	thread t2(sharedPtrFunc, p, 10000);

	t1.join();
	t2.join();


	cout << p.useCount() << endl;
	

	system("pause");
	return 0;
}

在上面的程序中,我们设置2个线程t1和t2,t1和t2都去执行sharedPtrFunc函数,函数内进行拷贝构造临时对象引用计数++,出函数作用域,临时对象析构,引用计数–。
在main()函数最后,我们打印一下当前的引用计数。按照常理,引用计数应该仍然为1.因为函数内拷贝的都应该正常析构了,如果不为1,说明有部分对象的析构函数中途切了出去,导致引用计数的错误。
在这里插入图片描述
我们发现引用计数的错误甚至到达了1534!那么对于这种情况该怎么办呢?答案就是加锁。定义一个互斥锁,对所有引用计数++或者–进行加锁处理,这样就能保障引用计数在操作的过程中能够不被切出去啦。

int AddRefCount()
	{
		// 加锁或者使用加1的原子操作
		_pMutex->lock();
		++(*count);
		_pMutex->unlock();
		return *count;
	}

	int SubRefCount()
	{
		// 加锁或者使用减1的原子操作
		_pMutex->lock();
		--(*count);
		_pMutex->unlock();
		return *count;
	}

shared_ptr所导致的循环引用的问题

当前的shared_ptr已经能解决绝大多数的问题了,但还是有一点点的瑕疵。就是在循环引用的时候还会造成内存泄漏
首先我们来了解一下什么是循环引用,这里我们用链表的例子来实现一下:

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
 
	ListNode(int x)
		:_data(x)
		, _prev(NULL)
		,_next(NULL)
	{}
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> cur(new ListNode(1));
	shared_ptr<ListNode> next(new ListNode(2));
	cur->_next = next;
	next->_prev = cur;
	cout << "cur" << "     " << cur.use_count() << endl;
	cout << "next" << "     " << next.use_count() << endl;
	return 0;
}

在这里插入图片描述
那么这种问题怎么解决呢?C++库为了解决这个问题,专门定义了一个叫做weak_ptr的东西,专门用于辅助shared_ptr来解决引用计数的问题。那他是怎么解决这么问题的呢?当shared_ptr内部要监视其他的shared_ptr对象时,类型就采用weak_ptr。这种weak_ptr在指向被监视的shared_ptr后,并不会使被监视的引用计数增加,且当被监视的对象析构后就自动失效。

然后它就什么都不管光是个删 , 也就是这里的cur和next在析构的时候 , 不用引用计数减一 , 直接删除结点就好。这样也就间接地解决了循环引用的问题,当然week_ptr指针的功能不是只有这一个。但是现在我们只要知道它可以解决循环引用就好。

智能指针是线程安全的吗?

对于unique_ptr,由于只是在当前代码块范围内有效。所以不涉及线程安全的问题。

对于shared_ptr,多个对象要同时共用一个引用计数变量,所以会存在线程安全的问题,但是标准库实现的时候考虑到了这一点,使用了基于原子操作(CAS)的方式来保证shared_ptr能够高效,原子的操作引用计数。

总结

1.不要使用auto_ptr,因为他的缺陷导致我们拷贝构造/赋值的时候有很大的麻烦。
2.在不需要拷贝构造/赋值的时候,可以使用unique_ptr。
3.有拷贝构造/赋值的情况,推荐使用shared_ptr.
4.类内有访问其他shared_ptr对象时,指针类型设为weak_ptr,可以不改其他其他shared_ptr对象的引用计数。
5.代码中尽量不用delete关键字,因为我们的内存的管理与释放全权交给对象处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值