C++-智能指针

智能指针

  • 智能指针的使用及原理
    • RAII
    • 智能指针的原理
    • std::auto_ptr
    • std::unique_ptr
    • std::shared_ptr
    • std::shared_ptr的循环引用、std::weak_ptr
  • C++11和boost中智能指针的关系

智能指针的使用及原理

  1. malloc出来的空间,没有进行释放,存在内存泄漏的问题。
  2. 异常安全问题。如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏。这种问题就叫异常安全。

解决以上问题需要用智能指针

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

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

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
#include
#include
using namespace std;
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	
	~SmartPtr()
	{
		if (_ptr)
		{
			cout << _ptr << endl;
			delete _ptr;
		}
			
	}

private:
	T* _ptr;
};
void MergeSort() 
{
	int* tmp = new int(1);
	SmartPtr<int> sp(tmp);
	
}
int main()
{
	try {
		MergeSort();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其像指针一样去使用。

#include
#include
using namespace std;

template<class T>
class SmartPtr {
public:
	SmartPtr(T* ptr = nullptr)
		: _ptr(ptr)
	{}
	~SmartPtr()
	{
		if (_ptr) 
			delete _ptr;
	}

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


struct Date
{
	Date()
		:_year(0)
		,_month(0)
		,_day(0)
	{}
	int _year;
	int _month;
	int _day;
};
int main()
{
	SmartPtr<int> sp1(new int);
	*sp1 = 10;
	cout << *sp1 << endl;

	SmartPtr<Date> sparray(new Date);

	sparray->_year = 2018;
	sparray->_month = 1;
	sparray->_day = 1;
}

总结智能指针的原理:

  1. RAII特性
  2. 重载operator*和opertaor->,具有像指针一样的行为。

std::auto_ptr

auto_ptr的实现原理:


	template<class T>
	class auto_ptr
	{
	public:
		// 1、RAII
		// 2、重载operator* 和 operator->  用起来像指针一样
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}

		// sp2(sp1) 管理权转移
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			sp._ptr = nullptr;
		}
		
		// ap2 = ap3;
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (this != &ap)
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}
			return *this;
		}

		~auto_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				cout << _ptr << endl;
			}
		}

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

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

	private:
		T* _ptr;
	};

注:

  • 这种方式是一种失败的设计,被质疑很久,一般公司都要求不要使用它。
  • C++98版本的库中就提供了auto_ptr的智能指针
  • C++库中的智能指针都定义在memory这个头文件中
  • auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
  • C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr

std::unique_ptr

unique_ptr的实现原理


	template<class T>
	class unique_ptr
	{
	public:
		// 1、RAII
		// 2、重载operator* 和 operator->  用起来像指针一样
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}

		// 防拷贝
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

		~unique_ptr()
		{
			if (_ptr)
			{
				delete _ptr;
				cout << _ptr << endl;
			}
		}

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

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

注意:

  • C++98 – 2011年中间这段,大家在使用boost库(第三方库)的智能指针

  • C++11 吸收了boost库中有价值的一些设计,将 boost – scoped_ptr shared_ptr weak_ptr这些智能指针转为C++11中的 – unique_ptr shared_ptr weak_ptr

  • 设计思路,有些场景下面,智能指针仅仅用于管理资源,不需要拷贝。

std::shared_ptr

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

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

模拟实现一份简答的shared_ptr


	template<class T>
	struct DefaultDel
	{
		void operator()(T* ptr)
		{
			delete ptr;
		}
	};

	template<class T,class D = DefaultDel<T>>
	class shared_ptr
	{
		template<class T>
		friend class weak_ptr;
	public:
		explicit shared_ptr(T* ptr=nullptr)
			:_ptr(ptr)
			,_pCount(new int)
			,_pMtx(new mutex)
		{}

		void add_ref()
		{
			_pMtx->lock();
			(*_pCount)++;
			_pMtx->unlock();
		}

		void release_ref()
		{
			bool flag = false;
			_pMtx->lock();
			if (--(*_pCount) == 0 && _ptr)
			{
				
				D del;
				del (_ptr);
				delete _pCount;
				flag = true;
				_ptr = nullptr;
			}
			_pMtx->unlock();
			if (flag)
				delete _pMtx;
		}

		shared_ptr(shared_ptr<T, D>& sp)
			:_ptr(sp._ptr)
			,_pCount(sp._pCount)
			,_pMtx(sp._pMtx)
		{
			add_ref();
		}


		/*shared_ptr& operator=(const shared_ptr& sp)
		{
		
			if (_ptr!=sp._ptr)
			{

				release_ref();
				
				_ptr = sp._ptr;
				_pCount = sp._pCount;
				_pMtx = sp._pMtx;
				add_ref();
			}
			return *this;
		}*/

		shared_ptr<T, D>& operator=(shared_ptr<T, D> sp)
		{
			swap(_ptr, sp._ptr);
			swap(_pCount, sp._pCount);
			swap(_pMtx, sp._pMtx);
			return *this;
		}

		~shared_ptr()
		{
			release_ref();
		}

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

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

		T* get()
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pCount;
		}
	private:
		T* _ptr;
		int* _pCount;
		mutex* _pMtx;
	};

  • 设计思路:多个智能指针对象管理一块资源,这块资源,对应一个引用计数,析构时–计数,计数等于0时表示是最后一个管理对象,就释放资源

注意:

  • shared_ptr的线程安全问题分为两方面:

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

补充:

  • 模拟实现删除器的传递位置跟std的不太一样
  • std的框架设计底层用一个类专门管理资源计数和释放,所以它可以再构造函数传参,把删除器类型传递给专门管理资源的这个类。
  • 而模拟实现的这个是一体化的,只能lc::shared_ptr给删除器,析构函数才能拿到删除器

std::shared_ptr的循环引用、std::weak_ptr

weak_ptr的原理的模拟实现:

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(shared_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{}

		weak_ptr(weak_ptr<T>& sp)
			:_ptr(sp._ptr)
			, _pCount(sp._pCount)
		{}

		weak_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pCount = sp._pCount;

			return *this;
		}

		weak_ptr<T>& operator=(weak_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pCount = sp._pCount;

			return *this;
		}
	private:
		T* _ptr;
		int* _pCount;
	};

注意:

  • weak_ptr不是常规意义的智能指针,没有接收一个原生指针的构造函数也不符合RAIl
  • weak_ptr的对象,可以访问指向节点资源但是不参与节点资源释放管理,其实就是不增加计数

循环引用举例:

// 循环引用
struct ListNode
{
	//问题:
	//std::shared_ptr _prev;
	//std::shared_ptr _next;
	//解决方案:
	std::weak_ptr<ListNode> _prev;
	std::weak_ptr<ListNode> _next;


	int val = 100;

	~ListNode()
	{
		cout << "~ListNode()" << endl;
	}
};


void test_cycle_ref()
{
	std::shared_ptr<ListNode> node1(new ListNode);
	std::shared_ptr<ListNode> node2(new ListNode);
	//std::shared_ptr n3(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;

	std::shared_ptr<ListNode> cur = node1;
	while (cur.get() != nullptr)
	{
		cur->val++;
		cout << cur->val << " ";
		cur = cur->_next.lock();
		// cur->_next.lock()  weak_ptr的lock用指向的资源构造了一个shared_ptr
	}
	cout<<endl;

}

int main()
{
	test_cycle_ref();
	return 0;
}

循环引用分析:

  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成员,所以这就叫循环引用,谁也不会释放

C++-智能指针_第1张图片
补充:
如果不是new出来的对象,可以使用shared_ptr (shared_ptr设计了一个删除器来解决这个问题)

C++-智能指针_第2张图片

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

  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

补充:

  • 内存泄漏解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。
  • RAII思想除了可以用来设计智能指针,还可以用来设计守卫锁,防止异常安全导致的死锁问题。
#include 
#include 
// C++11的库中也有一个lock_guard,
template<class Mutex>
class LockGuard
{
public:
	LockGuard(Mutex& mtx)
		:_mutex(mtx)
	{
		_mutex.lock();
	}
	~LockGuard()
	{
		_mutex.unlock();
	}
	LockGuard(const LockGuard<Mutex>&) = delete;
private:
	// 注意这里必须使用引用,否则锁的就不是一个互斥量对象
	Mutex& _mutex;
};
mutex mtx;
static int n = 0;
void Func()
{
	for (size_t i = 0; i < 1000000; ++i)
	{
		LockGuard<mutex> lock(mtx);
		++n;
	}
}
int main()
{
	int begin = clock();
	thread t1(Func);
	thread t2(Func);
	t1.join();
	t2.join();
	int end = clock();
	cout << n << endl;
	cout << "cost time:" << end - begin << endl;

	return 0;
}

你可能感兴趣的