C++:智能指针

内存泄漏的场景

下面由一个很经典的问题引入智能指针

int main()
{
   
	int* p = new int;
	return 0;
}

这是一个很经典的内存泄漏的情况,在向内存中申请了内存后却没有及时释放,此时这个内存就是所谓的内存泄漏,而内存泄漏是很严重的事故,会一直占用服务器或其他设备的内存,因此要尽可能的不出现这样的情况

看到这里可能会想,这样的情况也太容易发现了吧,这一下就能看出来会出现内存泄漏,但如果是这样的场景呢?

int div()
{
   
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
   
	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;
}

在这样的场景中,当new或者是后面的函数进行抛异常的时候,会导致语句直接跳到catch语句内,导致语句不能正常的被释放,就会在无形中导致内存泄漏的情况出现

因此在实际的操作过程中,内存泄漏的情况是很容易出现的,那么有没有一种途径,可以让内存不泄漏呢?能不能让这个指针自动进行释放呢?由此就引入了今天要总结的主题—智能指针

智能指针的使用原理

谈到智能指针,就不能不谈RAII

RAII

RAII,简单来说就是利用对象的生命周期来控制程序资源,核心点就是构造时获取资源,析构时释放资源,也就是说把管理资源的任务交给了一个对象,我们不再需要为管理这个资源而操心了,不仅不用显示的释放资源,而且还可以在生命周期内保持内容有效

下面举一个例子来说明一下RAII

template <class T>
class smartptr
{
   
public:
	smartptr(T* ptr)
		:_ptr(ptr)
	{
   }

	~smartptr()
	{
   
		cout << "delete" << ptr << endl;
		delete _ptr;
	}

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

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

首先定义一个智能指针,它的功能如上所示,当这个对象要被销毁的时候就会自动执行析构函数,这样就做到了内存的维护

int div()
{
   
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
   
	smartptr<int> p1(new int);
	smartptr<int> p2(new int);
	cout << div() << endl;
}

int main()
{
   
	try
	{
   
		Func();
	}
	catch (exception& e)
	{
   
		cout << e.what() << endl;
	}
	return 0;
}

对于正常和异常的运行结果如下所示

在这里插入图片描述
可以看出,智能指针是发挥出作用的,而这也是智能指针的最初版的理解,最简单的一种智能指针

但是现在的智能指针是有问题的,它不能实现赋值的功能,当遇到赋值的情景下,就会导致出现指针指向的空间被释放两次的情况出现,这是不被允许的

智能指针的发展

C++标准库中对于智能指针肯定是会有对应的分类的,在C++98中就引入了一款智能指针,这个智能指针叫做auto_ptr

auto_ptr

但是事实上,这个智能指针并没有得到认可,相反由于它本身实现的并不好,没有得到认可,下面看它的实现方式

	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()
		{
   
			cout << "delete" << _ptr << endl;
			delete _ptr;
		}

		auto_ptr<T>& operator=(auto_ptr<T>& sp)
		{
   
			_ptr = sp._ptr;
			sp._ptr = nullptr;
			return *this;
		}

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

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

这是一种使用管理权转换的方式实现的智能指针,原理就是把p2 = p1中的p1指针的指向改成空,这样就不用担心同一个内存被释放两次的情况了,但是这样的实现方法是有弊端的,当出现下面的场景时:

void smartptr1()
{
   
	auto_ptr<int> p1(new int);
	auto_ptr<int> p2(new int);
	p2 = p1;
	(*p1)++;
}

此时就会出现空指针的解引用情况,因此这样的写法是不被推崇的,基于这个原因,在后续的发展中延伸出了新的智能指针

unique_ptr

这个智能指针相比起来前者来说就显得靠谱了很多,它避免实现两次析构的方式是暴力防止,直接不允许用户实现赋值的操作

	template <class T>
	class unique_ptr
	{
   
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{
   }


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


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

		T* operator->()
		{
   
			return _ptr;
		}
	private:
		unique_ptr(unique_ptr<T>& sp) = delete;
		unique_ptr<T>& operator=(unique_ptr<T>& sp) = delete;
		T* _ptr;
	};

shared_ptr

上面的智能指针的通病是不能进行赋值和拷贝构造,为了解决这个问题就诞生了新的解决方案,shared_ptr

这个指针的原理是指针不仅有指向空间的功能,还能对有多少个指针指向这块空间有所标记,标记的方法就是前面所说过的引用计数的方法,基于引用计数的方法就能使得只有一个指针指向这段空间的时候才会将这块空间进行释放,如果有多于一个指针只想着这段空间,那么就不会进行释放

template <class T>
	class shared_ptr
	{
   
	public:
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
   
			_num = new int;
			*_num = 0;
		}

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

		~shared_ptr()
		{
   
			release();
		}

		void release()
		{
   
			if (*(_num) == 0)
			{
   
				cout << "delete-> " << _ptr << endl;
				delete _ptr;
				delete _num;
			}
			else
			{
   
				(*_num)--;
			}
		}

		// s2 = s1
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
   
			// 让s2和s1管理同一块指针和引用计数
			if (_ptr != sp._ptr)
			{
   
				release();
				_ptr = sp._ptr;
				_num = sp._num;
				(*_num)++;
			}
			return *this;
		}

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

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

		int use_count() const
		{
   
			return *_num;
		}

		T* get() const
		{
   
			return _ptr;
		}

	private:
		T* _ptr = nullptr;
		int* _num = nullptr;
	};

循环引用

上面所描述的智能指针已经可以应对绝大多数情况了,但是对于一些特殊的情况依旧不能完全处理,其中一个特殊情况就是循环引用,下面通过一个例子来说明什么是循环引用

// 循环引用
struct ListNode
{
   
	int val = 0;
	mysmartptr::shared_ptr<ListNode> next;
	mysmartptr::shared_ptr<ListNode> prev;

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

void shared_ptr1()
{
   
	mysmartptr::shared_ptr<ListNode> n1 = new ListNode;
	mysmartptr::shared_ptr<ListNode> n2 = new ListNode;

	n1->next = n2;
	n2->prev = n1;
}

那这是为什么呢?

在这里插入图片描述
看上面的图,从中其实可以看得出来一些原因,那么下面进行分析是为什么不能进行正确的释放

n2进行析构的时候,计数会减到0,n1析构,计数减到0

但是对于节点信息来说,就会产生如下的死循环

右边节点什么时候delete?答案是依靠左边节点里面的next
左边节点的next什么时候析构?答案是依靠左边节点delete就会把next成员析构
那左边节点什么时候析构?答案是右边节点的prev析构的时候,左边节点就会delete
那右边节点的prev什么时候析构?答案右边节点delete的时候,prev就析构了
那右边节点什么时候delete?就回到了最上面的问题

因此,基于这个原因,就会产生死循环,因此最后就都不会成功释放,也就不会析构成功了,变成了循环引用

基于这个原因,又引入了新的智能指针,weak_ptr

weak_ptr

这个指针是shared_ptr的弱化版本,它缺少了一个引用计数,通常会和shared_ptr搭配使用

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

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{
   }

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
   
			_ptr = sp.get();
			return *this;
		}

		// 像指针一样
		T& operator*()
		{
   
			return *_ptr;
		}

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

再把链表节点的指向进行改变,即可完成目的

// 循环引用
struct ListNode
{
   
	int val = 0;
	mysmartptr::weak_ptr<ListNode> next;
	mysmartptr::weak_ptr<ListNode> prev;

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

void shared_ptr1()
{
   
	mysmartptr::shared_ptr<ListNode> n1 = new ListNode;
	mysmartptr::shared_ptr<ListNode> n2 = new ListNode;

	n1->next = n2;
	n2->prev = n1;
}

相关推荐

  1. C++ 智能指针

    2023-12-08 14:36:01       51 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2023-12-08 14:36:01       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2023-12-08 14:36:01       100 阅读
  3. 在Django里面运行非项目文件

    2023-12-08 14:36:01       82 阅读
  4. Python语言-面向对象

    2023-12-08 14:36:01       91 阅读

热门阅读

  1. 【盘点世界十大著名黑客攻击事件】

    2023-12-08 14:36:01       53 阅读
  2. Android 开发中 常见的数据结构有哪些?

    2023-12-08 14:36:01       65 阅读
  3. 第6节:Vue3 调用函数

    2023-12-08 14:36:01       63 阅读
  4. spark3.x 读取hudi报错

    2023-12-08 14:36:01       42 阅读
  5. Apache Spark

    2023-12-08 14:36:01       50 阅读
  6. 浅析计算机网络安全的的防范与措施

    2023-12-08 14:36:01       38 阅读
  7. C++11改进单例模式

    2023-12-08 14:36:01       50 阅读