【C++】深入解析C++智能指针:从auto_ptr到unique_ptr与shared_ptr

文章目录

  • 前言:
  • 1. 智能指针的使用及原理
  • 2. C++ 98 标准库中的 auto_ptr:
  • 3. C++ 11 中的智能指针
    • 循环引用:
    • shared_ptr 定制删除器
  • 4. 内存泄漏
  • 总结:

前言:

随着C++语言的发展,智能指针作为现代C++编程中管理动态分配内存的一种重要工具,越来越受到开发者的青睐。智能指针不仅简化了内存管理,还有助于避免内存泄漏等常见问题。本文将深入探讨智能指针的使用及其原理,从C++98标准库中的auto_ptr开始,逐步过渡到C++11中更为强大和灵活的智能指针类型,如unique_ptrshared_ptr。此外,文章还将讨论循环引用问题、内存泄漏的原因及其危害,并提供相应的解决方案。通过本文的学习,读者将能够更好地理解和运用智能指针,编写出更安全、更高效的C++代码。

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

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内
存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在
对象析构的时候释放资源
。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做
法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// SmartPtr.h
// 使用RAII思想设计的smartPtr类

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

	~SmartPtr()
	{
		if (_ptr) {
			std::cout << "delete: " << _ptr << std::endl;
			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;
}
//test.cpp
#include <iostream>
#include "SmartPtr.h"
using namespace std;

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

	return a / b;
}

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);

	cout << div() << endl; 
}

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

	return 0;
}

在这里插入图片描述

  • 需要像指针一样的去使用:
// 像指针一样使用
T& operator*()
{
	return *_ptr;
}

T* operator->()
{
	return _ptr;
}
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(0));
*sp1 += 10;

SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1; // 等价于 sp3.opertor->()->second = 1;

cout << sp3->first << " " << sp3->second << endl;
  • 智能指针的拷贝问题
// 智能指针的拷贝问题
int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1);

	return 0;
}

在这里插入图片描述

vector / list.… 需要深拷贝,它们都是利用资源存储数据,资源是自己的。拷贝时,每个对象各自一份资源,各管各的,所以深拷贝。

智能指针 / 迭代器… 期望的是浅拷贝
资源不是自己的,代为持有,方便访问修改数据。他们拷贝的时候期望的指向同一资源,所以浅拷贝。而且智能指针还要负责释放资源。

itertor it = begin();

2. C++ 98 标准库中的 auto_ptr:

auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!

 // 智能指针的拷贝问题
// 1. auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
// 注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!
int main()
{
	std::auto_ptr<int> sp1(new int(1));
	std::auto_ptr<int> sp2(sp1);

	*sp2 += 10;

	// 悬空
	*sp1 += 10;

	return 0;
}

auto_ptr 的实现:

namespace hd
{
	template<class T>
	class auto_ptr {
	public:
		// RAII
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		// ap2(ap1)
		auto_ptr(auto_ptr<T>& ap)
		{
			_ptr = ap._ptr;
			ap._ptr = nullptr;
		}
	
		~auto_ptr()
		{
			if (_ptr) {
				std::cout << "delete: " << _ptr << std::endl;
				delete _ptr;                                                             
			}
		}
	
		// 像指针一样使用
		T& operator*()
		{
			return *_ptr;
		}
	
		T* operator->()
		{
			return _ptr;
		}
	
	private:
		T* _ptr;
	};
}

3. C++ 11 中的智能指针

boost 智能指针
scoped_ptr / scoped_array
shared_ptr / shared_array

C++ 11
unique_ptrscoped_ptr类似的
shared_ptrshared_ptr类似的

unique_ptr
禁止拷贝,简单粗暴,适合于不需要拷贝的场景
在这里插入图片描述
赋值也禁掉了:
在这里插入图片描述
unique_ptr:实现

namespace hd
{
	template<class T>
	class unique_ptr {
	public:
		// RAII
		unique_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}

		// ap2(ap1)
		unique_ptr(const unique_ptr<T>& ap) = delete;  // 禁掉拷贝构造
		// 赋值也要禁掉,赋值会生成默认成员函数,浅拷贝,也会出现问题
		unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;


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

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

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

	private:
		T* _ptr;
	};
}

如果必须要拷贝用shared_ptr:
shared_ptr 允许自由拷贝,使用引用计数解决多次释放的问题

引用计数: 记录有几个对象参与管理这个资源
在这里插入图片描述
shared_ptr 实现:
使用静态成员变量实现。

namespace hd
{
	template<class T>
	class shared_ptr {
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{
			_count = 1;
		}

		// sp(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			++_count;
		}

		~shared_ptr()
		{
			if (--_count == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
			}
			
		}

		int use_count()
		{
			return _count;
		}

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

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

	private:
		T* _ptr;
		
		static int _count;
	};

	template<class T>
	int shared_ptr<T>::_count = 0;
}

在这里插入图片描述
中释放了一个资源!
如果使用静态成员属于这个类,属于这个类的所有对象
需求:每个资源配一个引用计数,而不是全部都是一个引用计数!

所以,一个资源配一个引用计数无论多少个对象管理这个资源,只有这一个计数对象!
怎么找到这个引用呢?每个对象存一个指向计数的指针!

namespace hd
{
	template<class T>
	class shared_ptr {
	public:
		// RAII
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new int(1))
		{}

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}


		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				delete _ptr;
				delete _pcount;
			}
		}


		// 赋值 sp1 = sp3;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			if (_ptr != sp._ptr) // 避免自己给自己赋值
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		~shared_ptr()
		{
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

	private:
		T* _ptr;
		int* _pcount;
	};

}

在这里插入图片描述

shared_ptr 的缺陷:

// shared_ptr 的缺陷
struct ListNode
{
	int _val;
	std::shared_ptr<ListNode> _next;
	std::shared_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
		,_next(nullptr)
		,_prev(nullptr)
	{}

};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	n1->_next = n2;
	n2->_prev = n1;

	//delete n1;
	//delete n2;

	return 0;
}

循环引用:

  1. 左边的节点,是由右边的节点_prev管着的,_prev析构,引用计数减到 0, 左边的节点就是释放
  2. 右边节点中_prev 什么时候析构呢?右边的节点被delete时,_prev 析构。
  3. 右边节点什么时候delete呢?右边的节点被左边的节点的_next管着的,_next析构,右边的节点就释放了。
  4. _next 什么时候析构呢?_next 是左边节点的成员,左边节点 delete, _next 就析构了
  5. 左边节点什么时候释放呢?回调 1 点 又循环上去了

右边节点释放 -> _prev析构 -> 左边节点的释放 -> _next析构 -> 右边节点释放

所以这是 shared_ptr 特定场景下的缺陷, 只要有两个shared_ptr 互相管理就会出现这样的情况,所以即使用了智能指针,同样可能导致内存的泄漏。

struct ListNode
{
	int _val;
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;

	ListNode(int val = 0)
		:_val(val)
	{}

};

int main()
{
	std::shared_ptr<ListNode> n1(new ListNode(10));
	std::shared_ptr<ListNode> n2(new ListNode(20));

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	n1->_next = n2;
	n2->_prev = n1;

	cout << n1.use_count() << endl;
	cout << n2.use_count() << endl;

	//delete n1;
	//delete n2;

	return 0;
}

在这里插入图片描述
weak_ptr 可以通过不增加引用计数的方式,避免这个问题。(存在单独自己的 引用计数)
weak_ptr 不支持RAII, 不参与资源管理,不支持指针初始化,但是还是能起到指向你的作用
weak_ptr 的实现:

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

		weak_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
		}

		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{  
			_ptr = sp.get(); // 用 get方法调原生指针
		}

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

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

	private:
		T* _ptr;
	};
}

shared_ptr 定制删除器

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

// 定制删除器
int main()
{
	std::shared_ptr<ListNode> p1(new ListNode(10));
	std::shared_ptr<ListNode[]> p2(new ListNode[10]); // 可以用数组的

	std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArry<ListNode>()); // 用仿函数的对象去释放!
	std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr);  }); // 用lamada表达式也是可以的

	return 0;
}

在这里插入图片描述

定制删除器实现:

namespace hd
{
	template<class T>
	class shared_ptr
	{
	public:

		// function<void(T*)> _del = [](T* ptr) {delete ptr; };
		template<class D>
		shared_ptr(T* ptr, D del)
			:_ptr(ptr)
			, _pcount(new int(1))
			, _del(del)
		{}

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

		// sp2(sp1)
		shared_ptr(const shared_ptr<T>& sp)
		{
			_ptr = sp._ptr;
			_pcount = sp._pcount;

			// 拷贝时++计数
			++(*_pcount);
		}

		// sp1 = sp4
		// sp4 = sp4;
		// sp1 = sp2;
		shared_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			//if (this != &sp)
			if (_ptr != sp._ptr)
			{
				release();

				_ptr = sp._ptr;
				_pcount = sp._pcount;

				// 拷贝时++计数
				++(*_pcount);
			}

			return *this;
		}

		void release()
		{
			// 说明最后一个管理对象析构了,可以释放资源了
			if (--(*_pcount) == 0)
			{
				std::cout << "delete:" << _ptr << std::endl;
				//delete _ptr;
				_del(_ptr);

				delete _pcount;
			}
		}

		~shared_ptr()
		{
			// 析构时,--计数,计数减到0,
			release();
		}

		int use_count()
		{
			return *_pcount;
		}

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

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

		T* get() const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;

		std::function<void(T*)> _del = [](T* ptr) {delete ptr; };
	};

}

4. 内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{
   // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  
  delete[] p3;
}

总结:

本文详细介绍了智能指针的概念、使用和原理,从C++98的auto_ptr到C++11的unique_ptrshared_ptr,展示了智能指针在现代C++编程中的应用和发展。我们了解到RAII(资源获取即初始化)的设计模式,它通过将资源管理封装在对象的生命周期中,简化了资源的获取和释放过程。文章还讨论了智能指针的拷贝问题,特别是auto_ptr的缺陷和shared_ptr的循环引用问题,以及如何使用weak_ptr和定制删除器来解决这些问题。

此外,文章还探讨了内存泄漏的概念、原因和危害,以及如何在实际编程中避免这些问题。通过具体的例子和代码,我们学习了如何使用智能指针来管理资源,确保资源在使用完毕后能够被正确释放,从而避免内存泄漏和其他潜在的资源管理问题。

总的来说,智能指针是C++中一个强大的特性,它不仅提高了代码的安全性和效率,还使得资源管理变得更加简单和直观。通过本文的学习,读者应该能够更加自信地在C++项目中使用智能指针,编写出更加健壮和可靠的软件。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/644645.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

VMare下载安装

一.下载 1.百度搜索BROADCOM官网 打开官网&#xff1a; https://www.broadcom.com/​ 2.点击右上角&#xff0c;进行账号注册&#xff0c;注册好后&#xff0c;进行登陆 3.注册好后&#xff0c;进入个人界面&#xff1a;https://support.broadcom.com/#. 按下图所示点击进…

【多线程开发 2】从代码到实战TransmittableThreadLocal

【多线程开发 2】从代码到实战TransmittableThreadLocal 本文将从以下几个点讲解TransmittableThreadLocal(为了方便写以下简称ttl)&#xff1a; 前身 是什么&#xff1f; 可以用来做什么&#xff1f; 源码原理 实战 前身 ThreadLocal 要了解ttl就要先了解Java自带的类…

嵌入式全栈开发学习笔记---C语言笔试复习大全24

目录 内存管理 内存分配 堆和栈的区别&#xff1f;&#xff08;面试重点&#xff09; 申请内存的函数 malloc realloc free gcc工具链 编译的过程&#xff08;面试重点&#xff09; 第一步&#xff0c;预处理&#xff1a; 第二步&#xff0c;编译&#xff1a; 第三…

MySQL-数据库基础

一.MySQL安装 1.1卸载MySQL 把用户切换为root 查看是否有mysql数据库 ps axj | grep mysql 我这个是已经安装好的&#xff0c;为了更清楚的演示我把mysql关闭和mysql安装包卸载 关闭指令 systemctl stop mysqld查看是否在运行指令 systemctl stop mysqld查看安装包指令 …

校园招新之获取进QQ群但未报名人员

校园的社团、实验室招新一般由是校领导会发一个QQ通知&#xff0c;让各个班的同学们进一个招新群。 群里面会有负责人提示大家报名&#xff0c;但是群成员不总是都会报名&#xff0c;我们需要的就是&#xff0c;找到那些&#xff0c;已经进群&#xff0c;但是没有报名的同学&am…

网络原理 一

一、协议 网络通信中,协议是非常重要的概念. 协议进行了分层,此处就是按照这几层顺序来介绍每一层中的核心协议. 应用层,就对应着应用程序,是程序员打交道最多的一层,调用系统提供的 网络api 写出的代码都是基于应用层的. 应用层这里当然也有很多现成的协议,但更多的还是,程…

mysql实战——XtraBackup二进制包安装

1、二进制包下载网站 Software Downloads - Percona 2、安装xtrabackup 解压安装包 tar xvf percona-xtrabackup-8.0.27-19-Linux-x86_64.glibc2.17.tar.gz -C /usr/local 进入目录 cd percona-xtrabackup-8.0.27-19-Linux-x86_64.glibc2.17/ 安装依赖 yum install perl-Dig…

游戏子弹类python设计与实现详解

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、子弹类设计思路 1. 属性定义 2. 方法设计 三、子弹类实现详解 1. 定义子弹…

SimLab Composer v11.0.46 解锁版安装教程 (3D设计和逼真场景的多功能软件)

前言 SimLab Composer是由Simulation Lab公司推出的一款用于3D设计和逼真场景的多功能软件。该程序具有集成的图形环境&#xff0c;用于真实设计物理场景和对象&#xff0c;用户可以使用该软件中的工具设计简单到复杂和复杂。该程序的一个重要功能是能够构建和共享三维pdf文件…

揭秘章子怡成功之路:她是如何征服世界的?

章子怡的演艺生涯可谓是一部传奇❗❗❗ 从一个普通工人家庭的女孩&#xff0c;到如今的国际巨星 她的每一步都充满了努力和汗水 她的舞蹈基础为她日后的演艺事业奠定了坚实的基础 而她对戏剧和电影的热爱更是让她在演艺道路上不断前行 从《我的父亲母亲》到《卧虎藏龙》&…

【CTF Web】CTFShow web5 Writeup(SQL注入+PHP+位运算)

web5 1 阿呆被老板狂骂一通&#xff0c;决定改掉自己大意的毛病&#xff0c;痛下杀手&#xff0c;修补漏洞。 解法 注意到&#xff1a; <!-- flag in id 1000 -->拦截很多种字符&#xff0c;连 select 也不给用了。 if(preg_match("/\|\"|or|\||\-|\\\|\/|\…

Linux程序开发(十二):线程与多线程同步互斥实现抢票系统

Tips&#xff1a;"分享是快乐的源泉&#x1f4a7;&#xff0c;在我的博客里&#xff0c;不仅有知识的海洋&#x1f30a;&#xff0c;还有满满的正能量加持&#x1f4aa;&#xff0c;快来和我一起分享这份快乐吧&#x1f60a;&#xff01; 喜欢我的博客的话&#xff0c;记得…

window好用的网速工具

这是一个用于显示当前网速、CPU及内存利用率的桌面悬浮窗软件&#xff0c;并支持任务栏显示&#xff0c;支持更换皮肤。 github链接如下 https://github.com/zhongyang219/TrafficMonitor?tabreadme-ov-file

单窗口一天收益30+ 最新海外炼游地铁跑酷全自动黑科技项目,【软件+使用教程】

随着游戏界的最新狂潮&#xff0c;一款名为“海外酷跑”的游戏引起了全球玩家的关注。它继承了经典的地铁酷跑的精髓&#xff0c;同时注入了一种全新的元素&#xff1a;金币兑换虚拟货币的功能。玩家能够通过积极参与游戏、收集金币来兑换虚拟代币&#xff0c;每达到98000金币就…

详细分析Element中的MessageBox基本知识(附Demo)

目录 前言1. 基本知识2. Demo2.1 确认框2.2 警告框2.3 对话框 3. this.$confirm 前言 详细知识推荐阅读&#xff1a;详细分析Element Plus中的ElMessageBox弹窗用法&#xff08;附Demo及模版&#xff09; MessageBox则常用于Vue2 1. 基本知识 MessageBox 是 Element UI 提供…

每日一题——博弈论(枚举与暴力)

博弈论 题目描述 运行代码 #include<iostream> #include<vector> using namespace std; int main(){int n;cin >> n;vector<int> d(n,0);for(int i 0;i < n;i){cin >> d[i];}vector<int> in(1000,0);for(int k 1;k<3;k){for(int…

SpringBoot 集成 Nebula

工作需求&#xff0c;开始了解图数据库&#xff0c;经过工具选型&#xff0c;最终选择nebula graph&#xff0c;并集成到springboot&#xff0c;java 环境下如何对 Nebula Graph 进行操作&#xff0c;本文整理下过程。 1、首先引入 pom 依赖 <dependency><groupId&g…

【vue】el-select选择器实现宽度自适应

选择器的宽度根据内容长度进行变化 <div class"Space_content"><el-selectv-model"value":placeholder"$t(bot.roommessage)"class"select"size"small"style"margin-right: 10px"change"selectcha…

上5个B端系统的设计规范,让你的开发比着葫芦画瓢。

B端系统设计规范在企业级系统开发中起着重要的作用&#xff0c;具体包括以下几个方面&#xff1a; 统一风格和布局&#xff1a;设计规范能够统一系统的风格和布局&#xff0c;使不同功能模块的界面看起来一致&#xff0c;提升用户的使用体验和学习成本。通过统一的设计规范&am…

AI应用案例:影像报告智能辅助编辑系统

今天给大家介绍一个医疗行业的案例“影像报告智能辅助编辑系统”&#xff01;该案例已经在某三甲医院落地&#xff0c;模型准确度超过80%。 该项目上线后&#xff0c;保守估计&#xff0c;能为每位医生的每一张报告至少省下1分钟时间和2分钟的精力&#xff0c;20位初级医生&…