C++:智能指针

news2024/11/27 12:37:57

目录

一. 智能指针的概念及原理

1.1 什么是智能指针

1.2 智能指针的原理

二. 智能指针的拷贝问题

三. auto_ptr

3.1 auto_ptr的拷贝构造和赋值问题

3.2 auto_ptr的模拟实现

四. unique_ptr 

五. shared_ptr

5.1 shared_ptr的常用接口

5.2 shared_ptr的拷贝构造和赋值问题

5.3 shared_ptr的模拟实现

5.4 shared_ptr的循环引用问题

六. weak_ptr

七. 总结


一. 智能指针的概念及原理

1.1 什么是智能指针

智能指针RAII(Resource Acquisition Is Initialization),是一种利用对象的生命周期来管理资源的技术。如果我们采用传统的new/delete来申请和释放资源,如果忘记调用delete,或者在调用delete之前程序抛出异常,都会导致内存泄漏问题,如代码1.1,Func函数中的new p2和Div都可能抛异常,导致后面的delete没有执行从而引发内存泄漏,采用智能指针对资源进行管理,能够杜绝这类问题。

代码1.1:因异常引发的内存泄漏

int Div(int a, int b)
{
	if (b == 0)
		throw "除0错误";
	else
		return a / b;
}

void Func()
{
	int* p1 = new int[5];
	int* p2 = new int[5];

	//这里Div函数会抛异常,main函数会捕获异常,delete[]没有执行,引发内存泄漏
	int ret = Div(5, 0);

	delete[] p1;
	delete[] p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (std::exception& e)
	{
		std::cout << e.what() << std::endl;
	}
	catch (...)
	{
		std::cout << "未知错误" << std::endl;
	}

	return 0;
}

智能指针是一个类,在对象构造时调用构造函数获取资源,在对象生命周期内,保证资源不被释放,在对象生命周期结束时,编译器自动调用析构函数来释放资源。这就相当于,将管理资源的责任移交给了对象,这样即使程序抛出异常也不存在内存泄漏,因为捕获异常往往跳出函数体,执行流会离开对象的作用域,对象生命周期结束,编译器自动调用析构函数释放了资源。

采用智能指针管理资源,有如下优点:

  • 将资源管理的责任转移给智能指针对象,不用显示地释放资源,杜绝了异常安全问题。
  • 保证对象管理的资源在其生命周期内有效。

代码1.2定义了一种简易的智能指针SmartPtr,在其析构函数中会对资源进行释放。因为申请的资源可能是通过new T、new T[n]、malloc(...)这几种方法的任意之一来申请的,每种方式申请的资源需要不同的关键字/函数来释放资源,否则程序可能会崩溃。

因此,需要一个模板参数Del,这个模板参数用于接收仿函数,以匹配不同的资源释放方式。我们默认采用delete的方式释放资源,C++标准库中提供的智能指针,如果不显示给定仿函数来定义释放资源的方式,也是默认delete释放资源。

代码1.2:智能指针的简易实现 -- SmartPtr

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

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

template<class T>
struct Free
{
	void operator()(T* ptr) { free(ptr); }
};

template<class T, class Del = Delete<T>>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)  //构造函数
		: _ptr(ptr)
	{ }

	~SmartPtr()
	{
		Del del;
		del(_ptr);
	}

private:
	T* _ptr;  //管理的资源
};

1.2 智能指针的原理

智能指针,顾名思义,不能仅仅是用于资源管理,还应当具有指针的一般功能。因此,需要重载operator*、operator->函数(见代码1.3),用于访问指针指向的资源。注意:C++标准库中的智能指针均不重载operator[]函数。

智能指针原理总结:

  • RAII,管理资源,对象创建时申请资源,对象生命周期结束时释放资源。
  • 具有像指针一样的行为:operator*、operator->。

代码1.3:SmartPrt

template<class T, class Del = Delete<T>>
class SmartPtr
{
public:
	SmartPtr(T* ptr = nullptr)  //构造函数
		: _ptr(ptr)
	{ }

	~SmartPtr()
	{
		Del del;
		del(_ptr);
	}

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

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

private:
	T* _ptr;  //管理的资源
};

二. 智能指针的拷贝问题

我们要求智能指针具有一般的指针行为,因此,我们也就需要智能指针支持拷贝。但是,智能指针中的成员涉及到执行动态申请资源的指针,按照一般要求,应当进行深拷贝。

但是如果我们进行深拷贝,就会让两个智能指针指向不同的空间,但是我们所希望的是两个指针共同管理一块资源,因此我们就是要浅拷贝(值拷贝)。

但是值拷贝会存在对同一块空间多次释放的问题,对此,C++标准库中的智能指针auto_ptr和shared_ptr分别采用了管理权转移和引用计数的方法来解决问题,但一般会通过引用计数解决多次释放的问题(见第3、4章)。

智能指针拷贝问题总结:

  • 即使涉及到动态申请内存,智能指针的拷贝也不应为深拷贝,应当是浅拷贝。
  • 采用管理权转移(auto_ptr)或引用计数(shared_ptr)来解决同一块空间多次释放的问题。
  • 一般都使用引用计数来解决多次释放问题,auto_ptr大部分情况下不使用。
图2.1 智能指针拷贝问题

三. auto_ptr

3.1 auto_ptr的拷贝构造和赋值问题

auto_ptr采用管理权转移的方法进行赋值和拷贝构造,假设原先有一个auto_ptr对象p1,要通过p1构造p2,当拷贝构造完成后,用于拷贝构造传参的对象p1中管理资源的指针会被更改为nullptr,赋值也一样,假设p2=p1,p1中资源的管理权会转移给p2,p2原本的资源会被释放。

采用管理权转移的方法进行智能指针拷贝是一种极不负责任的行为,auto_ptr已经被很多公司明令禁止使用,一般项目中也极少使用auto_ptr。

图3.1 auto_ptr的拷贝构造
图3.2 auto_ptr的赋值

3.2 auto_ptr的模拟实现

这里最需要注意的问题是实现operator=函数时的自赋值检验,因为p2=p1要涉及到资源释放,如果p1和p2指向同一块资源,p2的资源被先行释放,那么p2=p1后,p2依旧指向原来的空间,但这块空间的使用权利已经换给了操作系统。

namespace zhang
{
	template<class T>
	class auto_ptr
	{
	public:
		//构造函数
		auto_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{ }

		//拷贝构造函数
		auto_ptr(auto_ptr<T>& ap)
			: _ptr(ap._ptr)
		{
			ap._ptr = nullptr;   //管理权转移
		}

		//赋值函数
		auto_ptr<T>& operator=(auto_ptr<T>& ap)
		{
			if (_ptr != ap._ptr)  //自赋值检验
			{
				delete _ptr;
				_ptr = ap._ptr;
				ap._ptr = nullptr;
			}

			return *this;
		}

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

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

		//析构函数
		~auto_ptr()
		{
			delete _ptr;
		}

	private:
		T* _ptr;
	};
}

四. unique_ptr 

unique直接将拷贝构造和赋值禁止,也就不存在浅拷贝的多次释放同一块空间的问题。

namespace zhang
{
	template<class T>
	class unique_ptr
	{
	public:
		//构造函数
		unique_ptr(T* ptr = nullptr)
			: _ptr(ptr)
		{ }

		//使用delete关键字强行禁止拷贝构造函数和赋值函数的生成
		unique_ptr(const unique_ptr<T>& up) = delete;
		unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

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

		//析构函数
		~unique_ptr()
		{
			delete _ptr;
		}

	private:
		T* _ptr;
	};
}

五. shared_ptr

shared是C++11标准库中新纳入的智能指针,它通过引用计数的方法,较好的解决了拷贝和赋值的问题,是实际项目中最常用的智能指针。

5.1 shared_ptr的常用接口

接口函数功能
shared_ptr(T* ptr = nullptr, Del del = Delete<T>())构造函数,del为定制删除器,是一个仿函数对象,用于不同情况下的资源释放操作
shared_ptr(shared_ptr<T>& sp)拷贝构造函数
shared_ptr<T>& operator=(shared_ptr<T>& sp) 赋值运算符重载函数
T& operator*()解引用操作符重载函数
T* operator->()成员访问操作符重载函数
T* get()获取shared_ptr内部管理资源的指针
long int use_count()

获取引用计数(当前智能指针管理的资源被多少智能指针共同管理)

bool unique()判断当前智能指针管理的资源是否只有它本身在管理(引用计数是否为1)

5.2 shared_ptr的拷贝构造和赋值问题

  • shared内部有一个成员变量long int* _pcount,它指向一块存储引用计数的空间,当进行拷贝构造时,引用计数+1,即:++(*_pcount)。
  • 进行赋值操作(sp2 = sp1)时,首先应当检查自赋值,如果是自赋值直接返回*this即可。如果不是自赋值,那么首先将sp2的引用计数-1,如果sp2的引用计数-1后变为了0,那么就释放sp2的资源,然后赋予sp2管理sp1管理的资源的权限,sp2和sp1共用一个引用计数,引用计数+1。
  • 调用析构函数时,先让引用计数-1,如果此时引用计数变为0,就释放资源。
图4.1 shared_ptr的拷贝构造问题
图4.2 shared_ptr的赋值问题

5.3 shared_ptr的模拟实现

namespace zhang
{
	template<class T, class Del = Delete<T>>
	class shared_ptr
	{
	public:
		//构造函数
		shared_ptr(T* ptr = nullptr)
			: _ptr(ptr)
			, _pcount(new long int(1))
		{ }

		//拷贝构造函数
		shared_ptr(shared_ptr<T>& sp)
			: _ptr(sp._ptr)
			, _pcount(sp._pcount)
		{
			++(*_pcount);  //引用计数+1
		}

		//赋值函数
		shared_ptr<T>& operator=(shared_ptr<T>& sp)
		{
			if (_ptr == sp._ptr)  //自赋值检查
			{
				return *this;
			}

			//this的引用计数-1,并判断是否需要释放资源
			if (--(*_pcount) == 0)
			{
				Del del;
				del(_ptr);
				delete _pcount;
			}

			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);

			return *this;
		}

		//指针获取函数
		T* get()
		{
			return _ptr;
		}

		//引用计数获取函数
		long int use_count()
		{
			return *_pcount;
		}

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

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

		bool unique()
		{
			return *_pcount == 1;
		}

		//析构函数
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				Del del;
				del(_ptr);
				delete _pcount;
			}
		}

	private:
		T* _ptr;   //指向动态申请空间的指针
		long int* _pcount;   //引用计数
	};
}

5.4 shared_ptr的循环引用问题

在绝大部分情况下,shared_ptr能够解决智能指针赋值造成的多次析构问题,也不会引发内存泄漏。但是,代码4.1展现了一种特殊情况,定义一个Node节点,其中包含两个shared_ptr成员_prev和_next。在主函数中实例化出两个shared_ptr<Node>对象n1和n2,n1的_next指向n2,n2的_prev指向n1,n1和n2相互指向对方,这样就属于循环引用,会造成n1和n2的资源释放失败,引发内存泄漏问题。

代码4.1:循环引用

struct Node
{
	int _val;
	std::shared_ptr<Node> _prev;
	std::shared_ptr<Node> _next;

	~Node()
	{
		std::cout << "~Node()" << std::endl;
	}
};

int main()
{
	std::shared_ptr<Node> n1(new Node);
	std::shared_ptr<Node> n2(new Node);

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

	return 0;
}

在代码4.1中,循环引用的成因如下:

  • 构造对象n1和n2,引用计数为1,然后n1->_next = n2、n2->_prev = n1后,引用计数变为2。
  • 先后由n2和n1调用析构函数,引用计数变为1。
  • 此时,n1和n2的资源还都没有释放,n1的_next依旧指向n2,n2的_prev依旧指向n1。
  • n1释放,就需要n2的_prev成员释放,n1释放,就需要n1的_next成员释放。但是,只有对象本身析构,它的成员才会析构,因此n1和n2彼此制约对方的析构,最终n1和n2的资源都无法释放,造成了内存泄漏。
图4.3 循环引用的原理

为了避免循环引用,可以把Node节点中的_next和_prev成员变量的类型改为weak_ptr<Node>,weak_ptr是C++标准库中的比较特殊的一个“智能指针”,允许使用shared_ptr对象来构造weak_ptr对象,但是,weak_ptr不增加引用计数,不参与资源的申请和释放,从严格意义上讲,weak_ptr不算是智能指针。

代码4.2:使用weak_ptr避免引用计数

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

	~Node()
	{
		std::cout << "~Node()" << std::endl;
	}
};

int main()
{
	std::shared_ptr<Node> n1(new Node);
	std::shared_ptr<Node> n2(new Node);

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

	return 0;
}

六. weak_ptr

weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,在weak_ptr中,也没有operator*函数和operator->成员函数,不具有一般指针的行为,因此,weak_ptr严格意义上并不是智能指针,weak_ptr的出现,就是为了解决shared_ptr的循环引用问题。

weak_ptr在进行拷贝构造和赋值时,不增加引用计数,由于weak_ptr不参与资源管理,也不需要显示定义析构函数来释放资源。

template<class T>
class weak_ptr
{
public:
	//默认构造函数
	weak_ptr()
		: _ptr(nullptr)
	{ }

	//拷贝构造函数
	weak_ptr(weak_ptr<T>& wp)
		: _ptr(wp._ptr)
	{ }

	//采用shared_ptr构造
	weak_ptr(shared_ptr<T>& sp)
		: _ptr(sp.get())
	{ }

	//赋值函数
	weak_ptr<T>& operator=(weak_ptr<T>& wp)
	{
		_ptr = wp._ptr;
	}

	//通过shared_ptr对象赋值
	weak_ptr<T>& operator=(shared_ptr<T>& sp)
	{
		_ptr = sp.get();
	}

private:
	T* _ptr;
};

七. 总结

  • 智能指针是用来对资源进行管理的类,在对象创建时动态申请内存资源,在对象生命周期结束时由编译器自动调用析构函数完成资源的释放,智能指针除了用来管理资源外,还应当具有指针的一般行为(operator*函数和operator->函数)。
  • 使用智能指针,相当于把资源管理的责任转交给智能指针对象。这样能够有效避免因为忘记delete或者程序抛出异常而引起的内存泄漏。
  • 智能指针应支持浅拷贝,但是浅拷贝存在同一块空间被多次释放的问题,为此,C++标准库中的三种智能指针auto_ptr、unique_ptr和shared_ptr分别采用了不同的方法来解决这一问题。
  • auto_ptr支持拷贝的方式是进行管理权转移,这是一种不负责任的处理方式,auto_ptr因此被许多公司禁止使用。
  • unique_ptr直接强行禁止拷贝构造和赋值。
  • shared_ptr通过引用计数的方式来进行浅拷贝,当引用计数为0时析构函数才释放资源,这样既支持了浅拷贝,也保证一块空间仅被析构一次。但是shared_ptr存在循环引用这一隐患,会造成内存泄漏。
  • 使用weak_ptr可以避免shared_ptr的循环引用问题,weak_ptr可以通shared_ptr对象来构造而不增加引用计数,weak_ptr不参与资源的管理,不支持operator*和operator->。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

软件工程导论(四)软件编码测试与维护

一、软件编程 1.1良好的编程习惯 变量命名有意义并且使用统一的命名规则 编写自文档代码&#xff08;序言性注释 or 行内注释&#xff09; 提前进行可维护性考量&#xff08;可以用常量的方式存在的数值最好以变量的方式存在&#xff09; 良好的视觉安排可以提高代码的可读性(…

ChatGPT训练一次要耗多少电?

如果开个玩笑&#xff1a;问ChatGPT最大的贡献是什么&#xff1f; “我觉得它对全球变暖是有一定贡献的。”知名自然语言处理专家、计算机科学家吴军在4月接受某媒体采访时如是说。 随着ChatGPT引爆AIGC&#xff0c;国内外巨头纷纷推出自己的AI大模型&#xff0c;大家为人工智…

2023 开放原子全球开源峰会“开发者之夜”高能剧透!

开发者之夜~即将高燃启动 最潮&#xff01;最嗨&#xff01;最青春&#xff01; 肆意&#xff01;亲切&#xff01;嗨 FUN 派&#xff01; 这是一场面向开发者的线下狂欢&#xff01; 也是一场精心准备的答谢盛宴&#xff01; 更是一场开源圈的老友聚会&#xff01; 开发者之夜…

IP地址中的子网掩码和CIDR

将常规的子网掩码转换为二进制&#xff0c;将发现子网掩格式为连续的二进制1跟连续0&#xff0c;其中子网掩码中为1的部分表示网络ID&#xff0c;子网掩中为0的表示主机ID。比如255.255.0.0转换为二进制为11111111 11111111 00000000 00000000。 ​ 在前面所举的例子中为什么不…

Yakit: 集成化单兵安全能力平台使用教程·Web Fuzzer篇

Yakit: 集成化单兵安全能力平台使用教程Web Fuzzer篇 1.数据包共享2.数据包扫描3.使用Web Fuzzer进行模糊测试4.常用 fuzz 标签5.热加载Fuzz1.数据包共享 分享/导入功能可用于信息分享,分享可以设置有效时长和分享密码,凭分享id和密码可以导入分享者的请求包 注意:数据包是…

uni-app 自定义组件之星级评价分数

效果图&#xff1a; 1.自定义组件starsRating.vue文件&#xff08;放在components文件夹内&#xff09; 代码截图&#xff1a; 对应的代码&#xff1a; <image click“btnStars1” class“starsicon” :src“starsObject[0]” mode“widthFix”> <image click“…

redis基础-----安装及使用场景基础操作

需要使用的网址 Redis中文网 Download | Redis 数据库及缓存架构选型网址&#xff1a; DB-Engines Ranking - popularity ranking of database management systems 常识&#xff1a; 存储方面&#xff1a; 磁盘&#xff1a; 1&#xff0c;寻址&#xff1a;ms 2&#xff…

达梦数据库读写分离集群异常测试(⾼可⽤)及双主(类似脑裂)问题处理

目录 测试前准备... 4 断电测试... 4 一、备库204断电... 4 二、断电数据新增测试... 5 1、备库204断电... 5 2、主库200新增数据&#xff0c;203备库查询正常... 5 3、204服务器启动并启动守护进程&#xff0c;测试&#xff0c;正常... 6 三、主库断电测试... 6 1、主…

python使用requests+excel进行接口自动化测试(建议收藏)

前言 在当今的互联网时代中&#xff0c;接口自动化测试越来越成为软件测试的重要组成部分。Python是一种简单易学&#xff0c;高效且可扩展的语言&#xff0c;自然而然地成为了开发人员的首选开发语言。而requests和xlwt这两个常用的Python标准库&#xff0c;能够帮助我们轻松…

archive log list :报错的解决办法

装好oracle数据库之后&#xff0c; 没事在练习sql语句&#xff0c; 看看一些基本的字典表啊啥的 但是当我执行 archive log list这个的时候居然给我报错&#xff0c; 这句话的意思是&#xff1a; 查看数据库的备份和恢复策略&#xff0c;并确定归档文件的具体位置&#xff…

小觅相机去畸变--Apriltag标签检测--Apriltag_ros

小觅相机型号:深度版,视场角50 ROS版本:nodelet 1.使用Calibrator获取相机的标定参数,或者用小觅相机自带的sdk获取: calibrator可以参考:ROS系统-摄像头标定camera calibration 小觅自带sdk参考:获取图像标定参数 或者小觅的ROS包编译后,会生成 这个get_img_para…

请求从前端到后端跟踪调试

请求慢的原因很多&#xff0c;当出现前端反应接口慢时&#xff0c;而通过后端日志查看请求处理时间并不慢时&#xff0c;往往会手足无措&#xff0c;当面对网络问题出现手足无措时&#xff0c;这就是在提醒你该抓包分析了&#xff0c;那么一般如何根据抓包文件去分析慢请求呢&a…

【MySQL】数据库报错集

一. 报错列表 1.1. Out of range value for column “xx” at row x 阐述&#xff1a;第 “x” 行的列 “xx” 超出范围 原因&#xff1a;建表时&#xff0c;类型bigint且长度20&#xff0c;如下字段的值超过其可输入的范围了 解决&#xff1a;修改该值为该列所设的长度即可 …

Cookie和Session原理详解

目录 前言 Cookie Session 会话机制 Cookie和Session的区别 Servlet中对Session和Cookie的封装 代码实例&#xff1a;实现用户登录 约定前后端交互的接口 前端页面&#xff1a; 后端实现 login index 总结 前言 在web的发展史中&#xff0c;我们知道浏览器和服务…

【模拟电子技术】理论考核回顾

写在前面&#xff1a; 1&#xff1a;好好学习&#xff0c;早日学会看B站华成英老师的课&#xff0c;不然就会像我一样最后快挂科了。 2&#xff1a;杂谈&#xff1a;我觉得一个“智者”可以因为我不会做题来侮辱我的智商&#xff0c;但是不能借此侮辱我没好好复习。 3&#…

AI换脸(支持视频换脸,支持cpu、低算力)【附代码】

可直接选择一张人脸去替换另一张图片或者视频中的人脸。本项目仅提供人脸替换部分&#xff0c;不需要数据集&#xff0c;不用训练&#xff01; 目录 项目说明 环境说明 准备工作 如何使用 免责声明 项目说明 本项目参考源码&#xff1a;GitHub - s0md3v/roop: one-click…

[数据结构 -- 手撕排序算法第一篇] 堆排序,一篇带你搞懂堆排序

目录 1、堆的应用 -- 堆排序 1.1 堆排序的思路分析 2、建堆 2.1 向上调整建堆&#xff1a;O(N*logN) 2.1.1 向上调整代码 2.1.2 向上调整建堆代码 2.2 向下调整建堆&#xff1a;O(N) 2.2.1 向下调整代码 2.2.2 向下调整建堆代码 3、堆排序实现代码 4、堆排序测试 1、…

一文读懂Serverless,它到底有啥用?

各位ICT的小伙伴好呀。 Serverless是最近大家讨论很多的一个话题。 今天我们就来聊聊什么是Serverless&#xff1f; ▉ Serverless是个啥&#xff1f; Server&#xff1a;服务器&#xff0c;Serverless解决问题的产品。 less&#xff1a;更少&#xff0c;Serverless解决问题…

如何解决空指针异常

NPE异常相信 Java 程序员都很熟悉&#xff0c;是 NullPointerException 的缩写&#xff1b;最近业务需求开发的有点着急&#xff0c;测试环境就时不时的来个NPE异常&#xff0c;特别的头疼&#xff1b;作为出镜率最高的异常之一&#xff0c;一旦入行Java开发&#xff0c;可以说…

微服务网关、SpringBoot、Nginx、tomcat8配置跨域

微服务网关、SpringBoot、Nginx、tomcat8配置跨域 跨域是什么?为什么会跨域解决跨域微服务网关处理跨域springboot项目配置跨域nginx配置跨域tomcat8配置跨域 跨域是什么? 跨域是A端向B端发送请求&#xff0c;A端与B端的地址协议、域名、端口三者之间任意一个不同&#xff0c…