【C++】智能指针

2024-01-02 08:00:23

1.智能指针的使用以及原理

1.1 RAII

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

当我们把管理一份资源的责任交给一个对象,就不需要自己显示是释放资源,使用这种方式可以让对象所需资源在其生命周期内都有效。就是在对象构造的时候获取资源,然后让对象需要的资源在它的生命周期内都有效,在对象析构的时候把资源释放。

举一个简单的例子:

template<class T>
class SmartPtr
{
public:

	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		delete _ptr;
        cout << "delete" << endl;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};


int main()
{
	SmartPtr<int> p(new int(2));

	return 0;
}

1.2 智能指针的原理

比如上面那个smartptr的例子,智能指针需要重载* 、以及重载->才可以像指针一样使用。

智能指针需要有RAII的特性以及重载operator*和opertaor->让其像指针一样。

1.3 std::auto_ptr

在c++98中就提供了auto_ptr的智能指针,下面就演示auto_ptr的使用以及问题

先简单模拟实现一个auto_ptr来理解他的原理

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

    auto_ptr<T>& operator=(auto_ptr<T>& ap)
     {
         
         if (this != &ap)
         {
         
             if (_ptr)
             delete _ptr;
         
             _ptr = ap._ptr;
             ap._ptr = NULL;
         }
         return *this;
     }

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

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

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


private:
	T* _ptr;
};

总结一下:auto_ptr通过转移控制权来实现智能指针。这样就不能实现两人智能指针同时指向一个资源。

1.4 std::unique_ptr

在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(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
	T* _ptr;
};

就是一个很简单的防拷贝。这样的智能指针也并不是很好用。

1.5 std::shared_ptr

C++11又提供了跟靠谱并且支持拷贝的的shared_ptr

shared_ptr的原理就是,通过添加引用计数来实现对象直接资源的共享,比如每天放学最后走的那位同学要关灯锁门,shared_ptr就是由最后一个人指向资源的对象来释放资源。

1.shared_ptr在对象内部,会对每个资源维护一个引用计数,用来记录这个资源对多少对象引用。

2.在对象销毁的时候,对引用计数--

3.引用计数如果是0就说明这个对象是最后一个指向这个资源的,在改对象销毁时就必须要释放该资源。

4.如果引用计数不是0,就说明并不是只有一个对象指向这个资源,就不能释放该资源。

shared_ptr的简单实现:

template<class T>

class shared_ptr
{
public:
	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp)
		,_pcount(sp._pcount)
	{
		(*_pcount)++;
	}

	T& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp)
		{

			release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

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


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


	int usr_count() const
	{
		return * _pcount;
	}

	T* get()
	{
		return _ptr;
	}

	void release()
	{
		if (--(*_pcount) == 0)
		{
			delete _ptr;
			delete _pcount;
		}
	}



	~shared_ptr()
	{
		release();
	}


private:
	T* _ptr;
	int* _pcount;

};

5. std::shared_ptr的循环引用

shared_ptr有一个重大缺陷就是循环引用。

举个例子:

struct ListNode
{
	int val;
	crin::shared_ptr<ListNode> next;
	crin::shared_ptr<ListNode> prev;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

void test_shared_ptr()
{
	crin::shared_ptr<ListNode> n1 = new ListNode;
	crin::shared_ptr<ListNode> n2 = new ListNode;
	n1->next = n2;
	n2->prev = n1;

}

在listnode对象销毁时打印delete。当两个shared_ptr对象互相引用是就会造成循环引用。

当调用test_shared_ptr函数时,并没有打印delete。这就说名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成员,所以这就叫循环引用,谁也不会释放。

他们都在等指向的对象释放,而指向的对象也再等指向的对象释放。这样就陷入了死循环。所以C++又提供了weak_ptr。

1.6 weak_ptr

weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr。

1. weak_ptr指针基本不单独使用,要和shared_ptr 类型指针搭配使用。

2.?weak_ptr并没有重载operator->和operator *操作符

解决循环引用的方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了

原理就是,n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和 _prev不会增加n1和n2的引用计数。

改成weak_ptr就完事了。

struct ListNode
{
	int val;
	crin::weak_ptr<ListNode> next;
	crin::weak_ptr<ListNode> prev;
	~ListNode()
	{
		cout << "delete" << endl;
	}
};

void test_shared_ptr()
{
	crin::shared_ptr<ListNode> n1 = new ListNode;
	crin::shared_ptr<ListNode> n2 = new ListNode;
	n1->next = n2;
	n2->prev = n1;


}

文章来源:https://blog.csdn.net/qq_63989073/article/details/135315612
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。