C++之智能指针

遇到的问题:

  在编写C++程序时,我们最常遇到的问题也就是内存方面的问题了。申请内存后未释放,打开文件后未关闭,这些都属于内存泄漏的问题。
  举个栗子:
如果我们在申请内存之后,程序抛了一个异常,并且我们的catch代码段也没有去做对应的处理,那么这个时候就会发生内存泄漏。如下程序:

int Division(int a, int b)
{
	if (b == 0) {
		throw "Division by zero condition";
	}
	return a / b;
}

int main()
{
	try
	{
		int *str = new int[1000];	//申请内存
		int a = 10;
		int b = 0;
		cout << Division(a, b) << endl;		//抛异常
		delete[] str;		//释放内存语句未执行
	}
	catch (const char* e)
	{
		cout << e << endl;	//没有对内存进行释放,导致内存泄漏
	}
	return 0;
}

上面这段代码可以看到在 try 代码段中我们申请了一块空间,但是当我们运行到Division 函数中时,它判断除数是 0 ,抛出异常,那么程序接下来会跳过后面的 delete[ ] str;而且我们也没有在 catch 代码块中进行释放内存的操作,因此会造成内存泄漏。

智能指针:

RAII:

  RAII 是一种利用对象生命周期来控制内存资源的一种技术,因为在面向对象的编程语言中,对象的创建和销毁分别是通过构造函数和析构函数来完成的,而且这两个函数都不用程序猿人为的控制,都是由系统自动调用的。
  我们可以将资源的申请放在构造函数中,将资源的释放放在析构函数中,这样即便程序异常退出,在对象生命结束的时候,系统也会自动调用析构函数去执行释放资源的语句。这样大大减少了内存泄漏一系列问题的产生。
  RAII带来的好处:
1、省去了我们人为释放资源的过程。
2、资源在对象生命周期内始终有效。

智能指针的实现:

原理:RAII 的思想 + 实现指针的特征(重载 *、->的操作)

std::auto_ptr:
auto_ptr 是 C++98版本就提供的一款智能指针。
auto_ptr 的问题:管理权转移,当对象发生拷贝或者赋值之后,前面对象就会悬空,导致前面的智能指针不能再用。
模拟实现:
下面的所有代码中的 A 类如下:

class A
{
public:
	A()
	{ cout << "A()" << endl; }
	~A()
	{ cout << "~A()" << endl; }
	int _num;
};

auto_ptr 模拟实现:

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

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

	//拷贝
	AutoPtr(AutoPtr& ap)
	{
		_ptr = ap._ptr;
		ap._ptr = nullptr;		//资源转移,管理权移交,ap的管理权失效
	}

	//赋值
	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;
	}
private:
	T* _ptr;
};

演示拷贝/赋值管理权转移:
在这里插入图片描述
std::unique_ptr:
为了解决这个管理权转移的问题,在 C++11 中引入了一个新的智能指针 unique_ptr,这个智能指针的做法非常粗暴,那么就是直接不让拷贝和赋值。
所以它的问题就是不能拷贝和赋值。
模拟实现:

template <class T>
class UniquePtr
{
public:
	UniquePtr()
		:_ptr(nullptr)
	{}
	~UniquePtr()
	{
		if(_ptr)
			delete _ptr;
	}
	T* operator->()
	{ return _ptr; }

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

不能赋值和拷贝肯定是不合理的,所以C++11又有了shared_ptr来解决这些问题。
std::shared_ptr:
原理:通过引用计数来解决不能拷贝和赋值的问题。
1、shared_ptr在其内部给每一份资源都维护了一个引用计数,通过引用计数来统计当前资源被几个对象共享。
2、在对象被销毁时,这份资源并不是直接释放,而是该资源的引用计数 -1 。直到引用计数减为 0,证明现在资源没有对象在使用,资源才会被释放。

问题:循环引用

模拟实现:(在模拟实现的过程中要注意,因为引用计数是被多个对象所共享的,所以在对引用计数进行 ++操作或者 --操作时要注意线程安全问题,要通过加锁来实现线程安全)

template <class T>
class SharedPtr
{
public:
	SharedPtr(T* ptr = nullptr)
		:_ptr(ptr)
		,_mutex(new mutex)
		,_Count(new int(1))
	{}
	~SharedPtr()
	{
		Release();
	}

	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		, _mutex(sp._mutex)
		, _Count(sp._Count)
	{
		AddRefCount();
	}

	SharedPtr<T>& operator=(const SharedPtr<T>& sp)
	{
		if (_ptr != sp._ptr) {
			Release();

			_ptr = sp._ptr;
			_mutex = sp._mutex;
			_Count = sp._Count;

			AddRefCount();
		}
		return *this;
	}

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

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

	int UseCount()
	{
		return *_Count;
	}

	void AddRefCount()
	{
		_mutex->lock();
		++(*_Count);
		_mutex->unlock();
	}
private:
	void Release()
	{
		bool flag = false;	//计数为0标志位
		_mutex->lock();
		if (--(*_Count) == 0) {
			delete _ptr;
			delete _Count;
			flag = true;
		}
		_mutex->unlock();

		if (flag == true)
			delete _mutex;
	}
private:
	T* _ptr;	//指向管理资源的指针
	mutex* _mutex;	//互斥锁
	int* _Count;	//引用计数
};

上面提到shared_ptr有一个问题:循环引用。
我们看看下面这种情况:

struct ListNode
{
	int data;
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	~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;
}

运行结果如图:
在这里插入图片描述
看起来好像没什么问题,在没有指向新的资源时,node1 和 node2 的引用计数都是 1. 指向新的资源后,引用计数变为 2。但是按照逻辑,程序执行完应该调用析构函数释放对象资源才对,这里并没有调用析构函数,这就是循环引用问题,这个问题导致了令人谈虎色变的内存泄漏问题。

我们将相互指向的两行代码注释掉,看看运行结果:
在这里插入图片描述
这样程序是走的正常的逻辑,当有对象释放资源时,引用计数 -1,引用计数减为 0 时,最终调用析构函数,资源彻底释放。
所以应该如何解决循环引用的问题呢?
主要有以下两种办法:
① 当要发生循环引用的时候,手动打破循环引用释放对象资源
② 使用弱引用指针 weak_ptr 来打破循环引用

第一种方法较为麻烦,所以第二种方法是最常用的方法。
将我们刚才的代码进行修改,将ListNode中的强引用智能指针改为弱引用智能指针。也就是说在有可能发生循环引用的地方都是用弱引用智能指针。

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

在这里插入图片描述
修改之后循环引用的问题也就迎刃而解了。


boost::scoped_ptr:
scoped_ptr 为了解决管理权转移问题也是非常简单粗暴直接防拷贝防赋值。

在boost库中有很多优秀的东西,比如 C++11 标准库中的uniqued_ptr、weak_ptr、shared_ptr都是参照boost库中的实验原理实现的。
在 boost 库中有一个 scoped_ptr ,在C++11标准库中的 uniqued_ptr 就是对应 boost 库中的scoped_ptr。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章