C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)

有的时候使用malloc或者new创建的对象忘记释放就会导致内存泄漏,又或者此时释放语句之前有一段代码是抛异常的话,那么执行流就会乱跳,导致内存也无法释放。
比如这一段代码,at越界访问会导致抛异常,导致执行流跳出从而没有释放指针p。

void Func()
{
	int* p = new int;
	vector<int> v;
	v.at(0) = 10;//会抛异常
	delete p;//导致p没有释放
}

因此引入了智能指针来防止这种问题导致的内存泄漏。

RAII思想

智能指针的思想就是RAII,就是利用对象的生命周期来管控资源的一种方式。
在对象构造时获取资源,把资源交给对象管理,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  1. 不需要显式地释放资源。
  2. 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针的思想RAII(交给对象管理)
template<class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		cout <<"delete:"<< _ptr << endl;
		delete _ptr;
	}
private:
	T* _ptr;
};

在对象的构造阶段把资源交给对象管理,对象的生命周期结束时会自动调用析构函数完成指针的释放。如图:抛出的异常被捕获,同时指针也释放了。
C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)_第1张图片

智能指针思想的变形(智能锁)

RAII思想可以扩展到锁上面,因为有时候加锁之后未解锁抛异常会导致执行流乱跳,而导致锁未释放,因此我们也可以基于此写一个智能锁,把这个锁交给一个对象管理,对象出了作用域就会自动调用析构函数完成锁的释放。但是这里需要注意的是,定义成员变量的时候要定义为引用,这样才能保证加锁的时候是加在同一个锁上的。

template<class T>//定义成模板就是如果有另外类型的锁来了也可以用这个SmartLock
class SmartLock
{
public:
	SmartLock(T& lock)
		:_lock(lock)
	{
		_lock.lock();
	}
	~SmartLock()
	{
		_lock.unlock();
	}
private:
	//使用引用就是为了保证加锁能加在同一个锁上面
	T& _lock;//引用定义变量就是在定义它的时候初始化它
};

使用如下:
smtlock管理了这个mtx,出了作用域会自动调smtlock的析构函数完成锁的释放。

void add(int n, int *value)
{
	SmartLock<mutex> smtlock(mtx);//用smtlock对象管理mtx这个锁,出了作用域会自动解锁
	for (int i = 0; i < n; i++)
	{
		++(*value);
	}
}
智能指针的两个要素
  1. RAII思想:把资源交给这个对象管理
  2. 能够像指针一样的行为(重载*->
template<class T>
class SmartPtr
{
public:
	//交给对象管理
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}
	~SmartPtr()
	{
		delete _ptr;
	}
	//像指针一样的行为
	T& operator*()//对象出了作用域还在,所以返回引用
	{
		return *_ptr;//返回这个对象
	}
	T* operator->()
	{
		return _ptr;//返回原生指针
	}
private:
	T* _ptr;
};

三种智能指针

1. auto_ptr(C++98)

C++98版本的库中提供了auto_ptr的智能指针。
想要实现一个智能指针就要实现这几个功能:RAII思想,像指针一样的行为。但是对象会有拷贝构造和赋值,auto_ptr的原理是进行了管理权的转移(管理权的转移就是,当把我的值给你之后,就把我置成空),这是一种带有缺陷的智能指针,会导致对象悬空

实现的原理如下:

template <class T>
class auto_ptr
{
public:
	auto_ptr(T* ptr)//构造
		:_ptr(ptr)
	{}
	~auto_ptr()//析构
	{
		delete _ptr;
	}
	T& operator*(){return *_ptr;}
	T* operator->(){return _ptr;}
	//拷贝构造
	//p1(p2)--->p2拷贝构造p1,此时把p2置空,管理权交给p1
	auto_ptr(auto_ptr<T>& ptr)//this指针是p1,ptr是p2
		:_ptr(ptr._ptr)
	{
		ptr._ptr = nullptr;
	}
	//赋值
	//p1=p2--->p2赋值给p1,此时把p2置空,管理权转移给p1
	auto_ptr& operator=(auto_ptr<T>& ptr)
	{
		if (this != &ptr)//不是自己给自己赋值
		{
			//如果p1不为空就把p1的资源先释放
			if (_ptr)
			{
				delete _ptr;
			}
			//再进行权限的转移
			_ptr = ptr._ptr;
			ptr._ptr = nullptr;
		}
		return *this;//支持连续赋值
	}
private:
	T* _ptr;
};

auto_ptr是一种管理权转移的思想,由于转移之后原来的对象会被置空,会导致对象的悬空,如下:

void test_auto_ptr()
{
	PTR::auto_ptr<int> p1(new int);
	PTR::auto_ptr<int> p2(p1);//拷贝构造把p1置空了
	//*p1 = 10;//p1悬空
	*p2 = 20;

	PTR::auto_ptr<int> p3(new int);
	p3 = p2;//赋值把p2置空了
	//*p2 = 30;//p2悬空
	*p3 = 40;
}

这里导致原来的对象悬空了,因此很多公司都会禁用这个auto_ptr。

2. unique_ptr(C++11)

C++11中开始提供更靠谱的unique_ptr。
unique_ptr是简单粗暴的防止拷贝,这种比较简单,效率高,但是功能不全面,不支持拷贝和赋值操作。

具体实现思想如下:

template<class T>
class unique_ptr
{
public:
	unique_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~unique_ptr()
	{
		delete _ptr;
	}
	T& operator*(){return *_ptr;}
	T* operator->()	{return _ptr;}
	//拷贝构造
	unique_ptr(unique_ptr<T>& ptr) = delete;
	//赋值
	unique_ptr<T>& operator=(unique_ptr<T>& ptr) = delete;
private:
	T* _ptr;
};

3. shared_ptr(C++11)

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
shared_ptr它的原理是引用计数,它的功能更加全面,支持拷贝构造。但是设计复杂,会有循环引用的问题。

那说到引用计数,我们就很容易想到当指向同一块空间时,每个对象都要用同一份引用计数,所以就直接把这个引用计数定为静态的,那么这样其实是不对的,因为万一有多个对象呢?这个static定义的计数变量全局就只有一个,会出现错误的(メ`ロ´)/。如下图:
C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)_第2张图片
这种情况,如果释放p4引用计数变为3,释放p3引用计数变为2,释放p2引用计数变为1,释放p2引用计数变为1,最后释放p1把第一个指针释放了,但是第二个指针就没有释放,就会导致内存泄漏。所以这里的引用计数必须是动态开辟的,每创建一个新的指针就要对应的给它开辟一个引用计数,如下图。
C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)_第3张图片
在shared_ptr内部有会给每个资源维护一份计数,只要指向的是同一块空间,那么引用计数就会进行++,当有人要释放时,先判断引用计数是不是1,如果不是1,那就说明还有别人指向这块空间,那我就不能释放了,因为我要是释放了这块空间,那么其他指向这块空间的指针就变成了野指针,此时只需要把引用计数 - - 就可以了。

但是如果是1说明只有一个人管理这块空间,也就是说我就是最后一个管理这块空间的人,那么此时就要完成释放(这里就可以象形的理解为:我是最后一个离开教室的人,那我就要把灯关掉)。

初次实现代码如下:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}
	~shared_ptr()
	{
		if (--(*_pcount) == 0)//引用计数--为0,此时要完成释放
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;//释放指针
			delete _pcount;//释放引用计数
		}
	}
	T& operator*(){return *_ptr;}
	T* operator->(){return _ptr;}
	//p2(p1)
	//拷贝构造-->你的就是我的,只要把你赋值给我就行,++引用计数即可
	shared_ptr(const shared_ptr<T>& ptr)
		:_ptr(ptr._ptr)
		, _pcount(ptr._pcount)
	{
		++(*_pcount);
	}
	//p1=p2
	//赋值-->
	shared_ptr<T>& operator=(shared_ptr<T>& ptr)
	{
		//if (this != &ptr)不算错,但是可以优化
		//两个对象的ptr如果相同就不进去了,否则--再++就是做无用功了
		//要是相同的对象,ptr相同就不作操作
		//要是不同的对象,你们指向同一块空间,ptr相同也不作操作
		if (_ptr != ptr._ptr)
		{
			//判断要赋值对象的引用计数是不是1
			//如果是1则需要释放这块空间,然后让该指针指向新的空间
			if (--(*_pcount) == 0)
			{ 
				delete _ptr;
				delete _pcount;
			}
			//两个指针指向同一块空间,再++引用计数
			_ptr = ptr._ptr;
			_pcount = ptr._pcount;
			++(*_pcount);
		}
		return *this;
	}
	int use_count()	{return *_pcount;}//查看引用计数是多少
private:
	T* _ptr;
	int* _pcount;//引用计数
};

基于上方的代码整体来看是没有问题,但是其实隐含了一个巨大的问题,那就是多线程的情况下会有线程安全的问题。因为这里的 ++操作和 - - 操作不是原子的操作 。所以我们还需要进一步完善shared_ptr。
看下面的代码我们就会发现多线程的时候释放的时候出问题了:

void copy_ptr(PTR::shared_ptr<int>& ptr, int n)
{
	//拷贝n次ptr
	for (int i = 0; i < n; i++)
	{
		PTR::shared_ptr<int> copy(ptr);
	}
}
void test_shared_ptr_safe()
{
	PTR::shared_ptr<int> p(new int);
	thread t1(copy_ptr, p, 1000);
	thread t2(copy_ptr, p, 1000);
	
	cout << p.use_count() << endl;//打印引用计数
	t1.join();
	t2.join();
}

t1和t2是我们创建了两个线程,这两个线程都去执行拷贝ptr的函数,并且执行1000次,此时程序运行完并没有打印释放的ptr地址,并且打印出来的引用计数也是不确定的,说明此时的引用计数已经出现了问题,导致内存没有释放,会出现内存泄漏的问题。
因此多线程编程时,要给这里的引用计数进行加锁操作,基于上面的代码我们进行改进:

template<class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}
	~shared_ptr()
	{
		Release();
	}
	T& operator*(){return *_ptr;}
	T* operator->(){return _ptr;}
	void Addref()//++引用计数操作
	{
		_pmtx->lock();
		++(*_pcount);
		_pmtx->unlock();
	}
	void Release()//--引用计数操作
	{
		_pmtx->lock();
		int flag = 0;
		if (--(*_pcount) == 0)
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
			delete _pcount;
			flag = 1;
		}
		_pmtx->unlock();
		if (flag == 1){	delete _pmtx; }
	}
	//拷贝构造-->你的就是我的,然后把引用计数++
	shared_ptr(const shared_ptr<T>& ptr)
		:_ptr(ptr._ptr)
		, _pcount(ptr._pcount)
		, _pmtx(ptr._pmtx)
	{
		Addref();
	}
	//赋值-->要判断是否需要把赋值对象的空间释放掉
	shared_ptr<T>& operator=(const shared_ptr<T>& ptr)
	{
		if (_ptr != ptr._ptr)
		{
			Release();//检查是否需要释放这个赋值对象
			_ptr = ptr._ptr;
			_pcount = ptr._pcount;
			_pmtx = ptr._pmtx;
			Addref();
		}
		return *this;
	}
	//查看引用计数
	int use_count(){return *_pcount;}
private:
	T* _ptr;
	int* _pcount;//引用计数
	mutex* _pmtx;
};

这样加锁之后,智能指针的引用计数的++和- - 是安全的,也支持拷贝构造,所以shared_ptr本身是安全的,智能指针的线程安全体现在引用计数的++和- -是线程安全的,但是它指向的对象不一定是线程安全的,因为智能指针指向的对象本身并不受它的管控。

shared_ptr的循环引用问题

struct ListNode
{
	std::shared_ptr<ListNode> _prev;
	std::shared_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};
void test_shared_ptr_cycle_ref()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);
	node1->_next = node2;
	node2->_prev = node1;
}
int main()
{
	test_shared_ptr_cycle_ref();
	system("pause");
	return 0;
}

这一段代码中用shared_ptr管理了node1和node2,同时也管理了node1和node2里面的_prev和_next,让智能指针share_ptr指向node1和node2时,两个智能指针的引用计数都是1,再让node1的_next指向了node2,让node2的_prev指向了node1,此时两个智能指针的引用计数都变成了2,出了test_shared_ptr_cycle_ref函数之后,node1和node2应该去调析构函数,但是此时智能指针的引用计数不为1,所以两个智能指针的引用计数- -,那么此时就没有完成节点的释放,因为发生了循环引用。
具体是这样的:
C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)_第4张图片

  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属于node1的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

那么这种情况下应该怎么办呢?
解决方案就是使用weak_ptr

struct ListNode
{
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;
	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};

因为在执行node1->_next = node2;node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数,这样就保证了正常的释放。

shared_ptr定制删除器

在上面写的代码中,都是一次只申请了一块空间,然后让智能指针管理,智能指针释放的时候直接调用delete即可,但是不排除有这么一种情况,那就是如果一次申请多块空间呢?那就要用delete[]来进行释放,因此可以利用仿函数来定制删除器。
具体代码如下:

template<class T>//定制删除器
struct DeleteArray
{
	void operator()(T* ptr)//仿函数:重载了()
	{
		delete[] ptr;
	}
};
struct test//定义一个用于测试的类
{
	~test()
	{
		cout << "~test()" << endl;
	}
};
void test_shared_ptr_delete()//测试的函数
{
	DeleteArray<test> del;
	std::shared_ptr<test> ptr(new test[10], del);
}
int main()
{
	test_shared_ptr_delete();
	system("pause");
	return 0;
}

这样就可以正常的释放了,结果如下:
C++:RAII思想和智能指针(auto_ptr,unique_ptr,shared_ptr)_第5张图片

C++11和boost中智能指针的关系

  1. C++ 98 中产生了第一个智能指针auto_ptr(管理权转移,带有缺陷)。
  2. C++ boost库给出了更实用的scoped_ptr和shared_ptr和weak_ptr。
  3. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。这里的unique_ptr对应的是boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

你可能感兴趣的:(C++)