首页 > 编程语言 >C++ 智能指针

C++ 智能指针

时间:2024-07-19 16:58:52浏览次数:8  
标签:numptr C++ 智能 int ._ shared new ptr 指针

一、为什么需要智能指针

看如下代码有什么问题:

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

如果div发生除零错误抛异常的话,此时p1 p2已经开辟好了,抛异常的话会跳转到catch语句而不会执行delete语句了,就会造成内存泄漏。

如果p2申请空间抛异常的话,p1一定是已经开辟好的,最后的delete p1语句就执行不了了,也会造成内存泄漏,虽然我们也可以通过抛异常的方式解决,但是如果开辟的空间比较多的话,代码就会一层套一层,不是很美观

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = nullptr;
	try
	{
		p2 = new int;
		try
		{
			cout << div() << endl;
		}
		catch (...)
		{
			delete p1;
			delete p2;
		}
	}
	catch (...)
	{
		delete p1;
	}
	delete p1;
	delete p2;
}

为了解决上述问题就引入了智能指针

二、智能指针的使用及原理

2.1 RAII

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

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

这种做 法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

接下来我们自己简单模拟实现一下RAII的智能指针

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
            delete _ptr;
   }
    
private:
    T* _ptr;
};
int div()
{
  int a, b;
  cin >> a >> b;
  if (b == 0)
  throw invalid_argument("除0错误");
  return a / b;
}
void Func()
{
  ShardPtr<int> sp1(new int); //智能指针
  ShardPtr<int> sp2(new int);//智能指针
  cout << div() << endl;
}
int main()
{
    try {
     Func();
   }
    catch(const exception& e)
   {
        cout<<e.what()<<endl;
   }
 return 0;
}

通过智能指针管理p1和p2,此时不论声请p2抛异常还是除零抛异常,跳转到catch语句后,sp1和sp2都会因为出了作用而调用析构函数释放资源,就不会发生内存泄露了,但是上述代码存在一些问题,当两个智能指针指向同一份资源时,就会释放两次资源而崩溃,接下来我们看看库里的智能指针是如何解决这个问题的。

2.2 std:: auto_ptr

auto_ptr是C++98提出来的,它解决这个问题的思路是管理权转移的思想,当另一个智能指针指向资源后,就将上一个智能指针置为空,始终保持只有一个智能指针管理一个资源

// C++98 管理权转移 auto_ptr
namespace zyq
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		auto_ptr(auto_ptr<T>& sp)
			:_ptr(sp._ptr)
		{
			// 管理权转移
			sp._ptr = nullptr;
		}
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			// 检测是否为自己给自己赋值
			if (this != &ap)
			{
				// 释放当前对象中资源
				if (_ptr)
					delete _ptr;
				// 转移ap中资源到当前对象中
				_ptr = ap._ptr;
				ap._ptr = NULL;
			}
			return *this;
		}
		~auto_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
	private:
		T* _ptr;
	};
}

但是auto_ptr的设计并不是很好,因为会导致原来的智能指针发生悬空问题,当我们再次使用原来的智能指针可能会发生访问空指针的问题,所以很多公司都禁止使用auto_ptr

int main()
{
 std::auto_ptr<int> sp1(new int);
 std::auto_ptr<int> sp2(sp1); // 管理权转移

 // sp1悬空
 *sp2 = 10;
 cout << *sp2 << endl;
 cout << *sp1 << endl;
 return 0;
}
2.3 std::unique_ptr

C++11中开始提供更靠谱的unique_ptr,它的思想很简单,直接将拷贝构造和赋值禁止掉,不让拷贝

  • C++11库才更新智能指针实现
  • C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr
  • C++11将boost库中智能指针精华部分吸收了过来
  • C++11->unique_ptr/shared_ptr/weak_ptr
// unique_ptr/scoped_ptr
// 原理:简单粗暴 -- 防拷贝
namespace bit
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
			{
				cout << "delete:" << _ptr << endl;
				delete _ptr;
			}
		}
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		unique_ptr(const unique_ptr<T>&sp) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>&sp) = delete;
	private:
		T* _ptr;
	};
}
2.4 std::shared_ptr

C++11中也提供了更加靠谱的shared_ptr,它与unique_ptr的不同点是他可以拷贝,那他是如何解决原来的问题的呢?它是通过引用计数的方式来解决释放两次资源的问题的,当发生拷贝时,会让这个资源的引用计数加1,当一个智能指针调用析构函数时,首先会让引用计数减1,如果此时引用计数为0的话表示没有智能指针指向这个资源了,就会释放掉这个资源,如果引用计数不为0的话,表示还有智能指针指向这个资源,此时就不会释放掉资源了。

模拟实现:

  因为引用计数涉及++ --的操作,在多线程的情况下并不安全,所以我们需要通过一定的手段让他变得线程安全

方法一:互斥锁

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_numptr(new int(1)),
			_pmtx(new mutex)
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}

		void AddRef()
		{
			_pmtx->lock();
			++(*_numptr);
			_pmtx->unlock();
		}

		void release()
		{
			_pmtx->lock();
			bool flag = false;
			if (--(*_numptr) == 0)
			{
				cout << "releasse" << endl;
				delete _ptr;
				delete _numptr;
				flag = true;
			}
			_pmtx->unlock();
			if (flag == true)
			{
				delete _pmtx;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			_pmtx = P._pmtx;
			AddRef();
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				_pmtx = P._pmtx;
				AddRef();
			}
			return *this;
		}

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

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

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		int* _numptr;
		mutex*  _pmtx;
	};
}

方法二:原子操作

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		shared_ptr(T* ptr)
			:_ptr(ptr),
			_numptr(new atomic<int>(1))
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}
		void release()
		{
			if (--(*_numptr) == 0)
			{
				cout << "releasse" << endl;
				delete _ptr;
				delete _numptr;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			(*_numptr)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				(*_numptr)++;
			}
			return *this;
		}

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

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

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		atomic<int>* _numptr;
	};

shared_ptr的缺点:循环引用

当发生类似如下的情况时,shared_ptr就会有点问题

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

int main()
{
   shared_ptr<ListNode> node1(new ListNode);
   shared_ptr<ListNode> node2(new ListNode);
   cout << node1.use_count() << endl;  //1
   cout << node2.use_count() << endl;  //1
   node1->_next = node2;
   node2->_prev = node1;
   cout << node1.use_count() << endl;  //2
   cout << node2.use_count() << endl;  //2
   return 0;
}
//当程序结束是两个智能指针的引用计数应该都为0,但是这种情况 两个人的引用计数都为1

问题分析:

我们假设node1指针的资源为节点1,node2指针的资源为节点2,当执行到return语句时,node2和node1会首先调用自己析构函数,此时两个节点引用计数--,变为了1,如果想让节点1的引用计数变为0的话就需要让node2的prev析构,那node2的prev什么时候析构呢?node2的prev是节点2管着的,当节点2释放prev就会释放,那节点2什么时候释放呢?当node1的next析构,节点2就会释放,而node1的next只有当节点1释放才会释放,这样节点1与节点2互相影响,就会造成循环引用的问题。为了解决这个问题,C++11也引入了weak_ptr, 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和 _prev不会增加node1和node2的引用计数。

2.5 剩余问题

如果不是new出来的对象如何通过智能指针管理呢?由于库的底层释放资源都是delete ptr实现的,如果我们用malloc申请资源怎么办呢?或者我们申请的类型显示定义出了析构函数并且我们申请了多个对象怎么解决呢?

其实shared_ptr设计了一个删除器来解决这个问题,我们可以在创建智能指针时将释放资源的方法也传进去,这样就解决了

// 仿函数的删除器
template<class T>
struct FreeFunc {
     void operator()(T* ptr)
     {
     cout << "free:" << ptr << endl;
     free(ptr);
     }
};
template<class T>
struct DeleteArrayFunc {
     void operator()(T* ptr)
     { 
     cout << "delete[]" << ptr << endl;
     delete[] ptr; 
     }
};
int main()
{
     FreeFunc<int> freeFunc;
     std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);
     DeleteArrayFunc<int> deleteArrayFunc;
     std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
    
     std::shared_ptr<A> sp4(new A[10], [](A* p){delete[] p; });
     std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p)
     {
         fclose(p); 
     });
 
     return 0;
}

接下来我们修改一下我们自己模拟实现的shared_ptr

namespace zyq
{
	template<class T>
	class shared_ptr
	{
	public:
		//构造
		template<class D>
		shared_ptr(T* ptr,D del)
			:_ptr(ptr),
			_numptr(new atomic<int>(1)),
			_del(del)
		{}

		shared_ptr()
		{}

		~shared_ptr()
		{
			release();
		}
		void release()
		{
			if (--(*_numptr) == 0)
			{
				cout << "release" << endl;
				_del(_ptr);
				delete _numptr;
			}
		}

		//拷贝构造
		shared_ptr(const shared_ptr<T>& P)
		{
			_ptr = P._ptr;
			_numptr = P._numptr;
			(*_numptr)++;
		}

		shared_ptr<T>& operator=(const shared_ptr<T>& P)
		{
			//自己给自己赋值有两种情况 sp1=sp1   sp2=sp3  sp2 sp3指向同一资源
			if (_ptr != P._ptr)
			{
				release();
				_ptr = P._ptr;
				_numptr = P._numptr;
				(*_numptr)++;
			}
			return *this;
		}

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

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

		int use_count()
		{
			return *_numptr;
		}

	private:
		T* _ptr;
		atomic<int>* _numptr;
		function<void(T*)> _del = [](T* ptr) {delete ptr; }; //不传定制删除器就采用默认的方式
	};
}

int main()
{
	shared_ptr<A> sp1(new A[5], [](A* ptr) {delete[] ptr; });
	shared_ptr<A> sp2((A*)malloc(sizeof(A)*5), [](A* ptr){free( ptr); });
}

标签:numptr,C++,智能,int,._,shared,new,ptr,指针
From: https://blog.csdn.net/m0_74910646/article/details/140550322

相关文章

  • 【C++】学习笔记——AVL树
    文章目录十六、AVL树1.AVL树的概念2.AVL树节点的定义3.AVL树的插入4.AVL树的旋转5.AVL树的验证6.完整代码+测试7.AVL树的性能未完待续十六、AVL树1.AVL树的概念二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素......
  • 守护安全,商业综合体消防安全视频AI智能解决方案全攻略
    据新闻报道,7月17日四川省自贡市某百货大楼发生大火,已造成16人遇难。经初步调查,此次火灾事故由施工作业引发,具体情况正在进一步调查中。随着城市化进程的加快,商场、百货大楼等商业综合体作为城市商业活动的重要载体,其消防安全工作显得尤为重要。传统的消防安全监控方式已......
  • 守护动物乐园:视频AI智能监管方案助力动物园安全与秩序管理
    一、背景分析近日,某大熊猫参观基地通报了4位游客在参观时,向大熊猫室外活动场内吐口水的不文明行为。这几位游客的行为违反了入园参观规定并可能对大熊猫造成严重危害,已经被该熊猫基地终身禁止再次进入参观。而在此前,另一熊猫基地也曾通报过游客向大熊猫活动场内扔甘蔗、石......
  • 15 指针 · 其三 · 进阶
    目录一、字符指针变量(一)字符指针与字符串数组区别二、数组指针变量(一)数组指针介绍(二)数组指针初始化(三)数组指针的使用三、二维数组传参的本质四、函数指针变量(一)函数指针介绍1、函数的地址2、函数指针的写法(二)函数指针的使用(三)两段有趣的代码(四)typedef五、函数......
  • C/C++ 《二级指针浅拷贝》
    背景A对象内部属性a属于int,动态分配内存回收,析构函数deleteA**aptr=newA[10]申请10个空间长度的A*类型测试浅拷贝测试代码#include<iostream>usingnamespacestd;classA{public:int*a;A(inti){//构造函数a=newint(i);}~A(......
  • C++数组中lower_bound和upper_bound函数的用法
    lower_bound函数首先,对于一个升序的数组(下标从0或者1开始是无所谓的,这里假设下标从1到n),即:a[1]<=a[2]<=a[3]<=...<=a[n]这个数列是(非严格)单调递增的。lower_bound(a+1,a+n+1,x)会返回a[1..n]中所有\(\gex\)的元素里面最小的那个数的地址。也就是说,......
  • C++信号处理
    什么是信号#include<csignal>或#include<signal.h>是处理信号的C-library。该库包含signal与raise两个功能函数。I.函数signal用于捕获信号,可指定信号处理的方式。II.函数raise产生一个信号,并向当前正在执行的程序发送该信号。signal()函数信号signal可以......
  • C++ 智能指针简单实现
    #pragmaoncetemplate<typenameTR>classjoker_shared_ptr{private:TR*ptr;size_t*count;public:constexprjoker_shared_ptr(/*args*/)noexcept{}constexprjoker_shared_ptr(std::nullptr_t)noexcept{}explicitjoker_sha......
  • 【学术会议征稿】第三届智能电网与能源系统国际学术会议(SGES 2024)
    第三届智能电网与能源系统国际学术会议20243rd InternationalConferenceonSmartGridandEnergySystems  第三届智能电网与能源系统国际学术会议(SGES2024)将于2024年10月25日-27日在郑州召开。   智能电网可以优化能源布局,让现有能源体系最大化的发挥其产能......
  • C++ 的 const 重载
    #include<iostream>#include<string>namespace{classA{public:constA&get_self()const{std::cout<<"常量版本"<<std::endl;return*this;}A......