C++ 智能指针的原理:auto_ptr、unique_ptr、shared_ptr、weak_ptr

news2024/10/6 8:36:52

目录

  • 一、理解智能指针
    • 1.普通指针的使用
  • 二、智能指针
    • 1.auto_ptr
    • 2.unique_ptr
    • 3.shared_ptr
      • (1)了解shared_ptr
      • (2)shared_ptr的缺陷
    • 4.weak_ptr

本文代码在win10的vs2019中通过编译。

一、理解智能指针

1.普通指针的使用

  如果程序需要动态申请内存空间,我们通常使用malloc或者new从堆上申请空间,从堆上申请的空间是需要释放的,否则就会造成内存泄漏。

  不同的申请方式也对应着不同的释放方式,malloc申请的空间通过free释放,new申请的空间通过delete释放。

  正常来讲,如果我们从堆上申请了空间,那么在空间使用完毕后就需要将空间释放,但是如果程序在运行时出错,直接崩溃,那么我们的空间就来不及释放,从而造成资源泄露。

  下面这段代码因为试图对空指针解引用,因此会崩溃,所以ptr指向的空间就没有释放,造成资源泄露:

#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;

int main() {	
	int* ptr = new int;//ptr指针申请了空间

	int* pn = nullptr;//pn指针指向空
	cout << *pn;//对空指针解引用,造成崩溃

	delete ptr;//释放ptr申请的空间(因为程序崩溃,无法运行到这里)
}

  因此,如果要避免这种情况,就需要在所有可能会中途退出的位置之前释放资源,或者是使用异常来解决。但这样的解决办法会让代码变得复杂、庞大。

  因此我们需要一种智能的指针,让指针使用完毕后自动释放申请的空间,也就是智能指针。

  我们可以自己来实现一个简单的智能指针,当然会有很多缺陷,但主要是了解一下思想。想一想类的构造函数和析构函数,生成一个类的对象时,调用构造方法,销毁类的对象时,调用析构方法。因此我们可以在构造函数中传入空间地址,在析构函数中释放资源,这样就可以把一个类的对象当指针一样使用了。

  看看下面这段代码:

#define _CRT_SECURE_NO_WARNINGS
#include "iostream"
using namespace std;

template<typename T>
class SmartPtr {
public:
	T* ptr;//指针
public:
	SmartPtr(T* _p = nullptr)
		:ptr(_p)
	{}

	~SmartPtr() {
		if (ptr != nullptr) {
			delete ptr;//释放空间
		}
	}

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

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


struct Num {
	int a;
	int b;
};

void Test() {
	//new会从堆上申请空间并返回空间首地址,将空间首地址作为参数传递
	SmartPtr<int> s1(new int);
	*s1 = 10;//通过重载的*运算符修改空间中的值

	SmartPtr<Num> s2(new Num);
	s2->a = 11;//通过->运算符直接访问结构体对象的成员变量
	s2->b = 12;
}

int main() {
	Test();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

  但是这个简单版本的智能指针有一个很大的缺点,浅拷贝。编译器生成的默认拷贝构造函数和赋值运算符重载都是浅拷贝,如果通过拷贝构造和赋值运算符重载来给其他对象赋值,当对象生命周期结束调用析构函数的时候,就会因为多次释放同一个资源造成程序崩溃。

  但是不能因为要解决浅拷贝的问腿,就自己定义深拷贝的拷贝构造函数和赋值运算符重载,因为智能指针是把对象当指针一样使用,指针只负责管理资源。如果我们进行深拷贝,那么多个对象就不能使用同一份资源了。

二、智能指针

  为了解决浅拷贝的问腿,来介绍C++中的智能指针,这里介绍四种:auto_ptr(不推荐使用,仅作了解)、unique_ptr(直接禁止拷贝)、shared_ptr、weak_ptr。

1.auto_ptr

  解决浅拷贝的方式:资源转移,如果要用b给a赋值(或者是拷贝构造),就直接把b的资源转移给a,然后让b指向空。(所以b就不能使用这个资源了)

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std;

void Test() {
	auto_ptr<int> ap1(new int);
	*ap1 = 10;

	auto_ptr<int> ap2(ap1);//用ap1拷贝构造ap2
	*ap2 = 12;
}

int main() {
	Test();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

  来看看auto_ptr的缺陷:如图所示,用ap1拷贝构造ap2后,ap1的指针就置为空了(因为资源从ap1转移给ap2了,ap1以后就不能使用这个资源了)。因此不推荐使用,不然很可能在不知情的情况下就去对ap1解引用了。

资源转移

2.unique_ptr

  解决浅拷贝的方式:禁止拷贝,只要不能拷贝,就不会有浅拷贝发生。(从根源解决问题)

  如果确定这个资源只会被一个指针管理,就使用unique_ptr。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std;

void Test() {
	unique_ptr<int> up1(new int);
	*up1 = 10;

	//unique_ptr<int> up2(up1); 用up1拷贝构造up2会报错
	//unique_ptr<int> up3 = up1; 用up1给up2赋值会报错
}

int main() {
	Test();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

3.shared_ptr

(1)了解shared_ptr

  解决浅拷贝的方式:引用计数。通过计数来判断当前有多少个shared_ptr指向了这个被管理的资源,当计数变成0,说明此时没有指针管理这个资源,因此可以释放这个资源。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std;

void Test() {
	shared_ptr<int> sp1(new int);
	*sp1 = 10;
	shared_ptr<int> sp2(sp1);
	*sp2 = 11;
	*sp1 = 12;

	cout << "sp1管理的资源同时被 " << sp1.use_count() << " 个智能指针管理" << endl;
}

int main() {
	Test();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

  如图所示:sp1和sp2同时管理资源,资源的地址是一样的,说明两个指针管理的是同一个资源。

证明

  shared_ptr中有两个变量,一个变量(ptr)指向资源的空间首地址,另一个变量(pcount)指向引用计数的空间。引用计数空间的值初始状态是1,每当有一个智能指针管理了这个资源,就会将这个资源对应的引用计数加1,每当有一个智能指针不再管理这个资源,就会把引用计数减1,当引用计数变成0,说明已经没有指针管理这个资源,因此就可以释放这个资源了。

(2)shared_ptr的缺陷

  shared_ptr有一个很大的缺陷:循环引用

  如下代码,定义了链表结点结构体 struct ListNode,该结构体包含了三个成员变量,value用来保存该结点的值,next用来指向下一个链表结点,prev用来指向前一个链表结点。next和prev都是shared_ptr类型的智能指针变量。

  在函数Testshared()中,使用智能指针shared_ptr管理ListNode对象,然后更新它们的指针指向,让两个链表结点连接起来。

  这份代码运行完毕以后会造成资源泄露。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std;

struct ListNode {
public:
	shared_ptr<ListNode> next;//next指向下一个链表结点
	shared_ptr<ListNode> prev;//prev指向前一个链表结点
	int value;
public:
	ListNode(const int data=0) 
		:next(nullptr)
		,prev(nullptr)
		,value(data)
	{}

	~ListNode() {
		cout << "析构函数" << endl;
	}
};

void Testshared() {
	shared_ptr<ListNode> sp1(new ListNode(4));
	shared_ptr<ListNode> sp2(new ListNode(5));
	sp1->next = sp2;
	sp2->prev = sp1;
}

int main() {
	Testshared();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

  来看看为什么会造成资源泄露。

  下图是sp1和sp2刚构建好的样子,智能指针内部的ptr指针指向链表结点(资源)的首地址,pcount指针指向存储引用计数的空间。

  刚创建出来sp1和sp2时,由于每个链表结点都只被一个智能指针管理,因此引用计数都是1。

  下图是 sp1->next = sp2; sp2->prev = sp1; 执行后的情况,因为ListNode结构体内部的next和prev是用shared_ptr实现的,因此next和prev也具有指针ptr和引用计数pcount。

  由于ListNode2被ListNode1的next指向,也就代表着多了一个指针管理ListNode2,因此ListNode2的引用计数需要加1。

  ListNode1被ListNode2的prev指向,代表着多了一个指针管理ListNode1,因此ListNode1的引用计数需要加1。

  当Testshared函数执行完毕,作为局部对象的sp1和sp2就需要被销毁,后创建的先销毁,因此首先销毁sp2。

  sp2的ptr与ListNode2断开连接,然后会将引用计数空间的值减1,引用计数变成1,代表还有1个指针在管理ListNode2,因此不能释放ListNode2的资源。

  然后销毁sp1,sp1同样先断开与ListNode1的联系,然后将引用计数空间的值减1,引用计数变成1,代表还有1个指针在管理ListNode1,因此不能释放ListNode1的资源。

  然后现在就尴尬了,如果要释放ListNode1,就要先把ListNode1的引用计数置为0,因此需要让ListNode2的prev不要指向ListNode1,而ListNode2的prev是ListNode2的成员变量,因此就必须要释放ListNode2才能让prev断开与ListNode1的联系。

  但是要释放ListNode2就需要把ListNode2的引用计数置为0 ……

  这样就成套娃了,因此直到最后也没有释放资源,析构函数没有调用,造成了内存泄漏。

  如下图:

4.weak_ptr

  weak_ptr无法单独管理资源。

  weak_ptr就是用来解决shared_ptr的循环引用的,将weak_ptr和shared_ptr搭配使用,就可以解决循环引用的问题。

  在上面介绍shared_ptr的时候,为了方便理解,因此直接说的是引用计数。实际上shared_ptr的引用计数有两个:_Uses 和 _Weaks。(两个值初始值都是1)

  一个资源每当被一个shared_ptr智能指针管理,那么_Uses加1。一个资源每当被weak_ptr智能指针管理,_Weaks加1。

  因此_Uses 和 _Weaks就表示了这个资源被多少个weak_ptr智能指针或者多少个shared_ptr智能指针管理。

  当引用计数空间的 _Uses 变成0,说明被管理的资源可以释放了。当_Uses 和 _Weaks都变成0,说明引用计数空间可以被释放了。

  将前一段代码进行改造,将next和prev改为weak_ptr智能指针,同时要去掉构造函数中的相关内容。因为weak_ptr不能单独管理资源,就连空指针都不允许。

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <memory>
using namespace std;

struct ListNode {
public:
	weak_ptr<ListNode> next;//next指向下一个链表结点
	weak_ptr<ListNode> prev;//prev指向前一个链表结点
	int value;
public:
	ListNode(const int data = 0)
		: value(data)
	{}

	~ListNode() {
		cout << "析构函数" << endl;
	}
};

void Testshared() {
	shared_ptr<ListNode> sp1(new ListNode(4));
	shared_ptr<ListNode> sp2(new ListNode(5));
	sp1->next = sp2;
	sp2->prev = sp1;
}

int main() {
	Testshared();
	_CrtDumpMemoryLeaks();//检测内存泄漏
}

  通过图片来理解改造后的代码,下图是刚创建出来sp1和sp2的情况。sp2和sp1指向的引用计数空间中有两个值:_Uses 和 _Weaks。(初始状态都是1)

初始

  当 sp1->next = sp2; sp2->prev = sp1; 运行后,ListNode1的next指向了ListNode2,ListNode2的prev指向了ListNode1。

  ListNode1的next指向ListNode2,因此next中的_Ptr指向ListNode2的空间首地址,_Rep指向ListNode2的引用计数空间。另一方同理。

  因为next和prev是weak_ptr智能指针,因此两方的引用计数空间的_Weaks都要加1,_Weaks变成2。

  如图:

中间

  当Testshared函数执行完毕,作为局部变量的sp1和sp2就要被销毁,后创建的先销毁,因此先销毁sp2。

  首先断开sp2的_Ptr和ListNode2的联系,因此该链表结点对应的引用计数空间的_Uses要减1,_Uses变成0,说明可以释放ListNode2了,因此需要把ListNode2的prev与ListNode1断开联系,因此ListNode1对应的引用计数空间的_Weaks需要减1,_Weaks变成1。

  把ListNode2的资源处理干净后,就需要把ListNode2对应的引用计数空间的_Weaks减1,然后sp2就可以释放了。ListNode2对应的引用计数空间的_Weaks变成1,说明该链表结点对应的引用计数空间还不能释放。
销毁sp2
  释放了sp2后就需要释放sp1了,首先断开sp1的_Ptr和LsitNode1的联系,这样该链表结点对应的引用计数空间的_Uses就需要减1,_Uses变成0,说明ListNode1的资源可以释放了,这样就需要断开next和其他结点的联系,然后ListNode2对应的引用计数空间的_Weaks就需要减1,_Weaks变成0,说明引用计数空间可以释放了。

  ListNode1的资源处理后,ListNode1对应的引用计数空间中的_Weaks就需要减1,然后_Weaks变成0,说明这个引用计数空间可以被释放了。

  析构函数此时也被正常调用了,shared_ptr的循环引用被解决了。
最后

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

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

相关文章

2.22.1、死锁的概念

1、死锁的概念 1.1、什么是死锁 我等待你&#xff0c;你等待他&#xff0c;他等待她&#xff0c;她等待我…这世界每个人都爱别人… 我们从资源占有的角度来分析&#xff0c;这段关系为什么看起来那么纠结… 在并发环境下&#xff0c;各进程因竞争资源而造成的一种互相等待对方…

ChatGPT入门案例|张量流商务智能客服

本篇介绍了序列-序列机制和张量流的基本概念,基于中文语料库说明基于循环神经网络的语言翻译的实战应用。 01、序列-序列机制 序列-序列机制概述 序列-序列(Sequence To Sequence,Seq2Seq)是一个编码器-解码器 (Encoder-Decoder Mechanism)结构的神经网络,输入是序列(…

【C#个人错题笔记】

观前提醒 记录一些我不会或者少见的内容&#xff0c;不一定适合所有人 字符串拼接 int a3,b8; Console.WriteLine(ab);//11 Console.WriteLine("ab");//ab Console.WriteLine(a""b);//38 Console.WriteLine("ab"ab);//ab38 Console.WriteLine…

17个优秀WordPress LMS在线教育平台主题

为您的WordPress在线教育平台主题网站选择在线课程主题是您在建立在线教育业务时做出的最重要的决定之一。正确的主题不仅决定了您网站的外观和感觉&#xff0c;还决定了用户体验。这在构建在线课程平台时变得更加重要&#xff0c;因为您的访问者将是您的学生&#xff0c;他们会…

nodejs基于vue疫情期间网课管理系统

随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于疫情网课管理系统当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了疫情网课管理系统&#xff0c;它彻底改变了过去传统…

内网渗透(四十四)之横向移动篇-DCOM远程执行命令横向移动

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

波次分拣系统

一、系统架构&#xff1a; v1.2基站软件管理系统仓库标签v1.4仓库标签二、系统简介&#xff1a; 标签系统主要由标签服务器&#xff0c;基站&#xff0c;电子标签前三部分组成&#xff0c;操作界面借助于京东仓库已有的作业电脑来实现&#xff0c;标签服务器与WMS进行数据对接。…

罗技LogitechFlow技术--惊艳的多电脑切换体验

作者&#xff1a;Eason_LYC 悲观者预言失败&#xff0c;十言九中。 乐观者创造奇迹&#xff0c;一次即可。 一个人的价值&#xff0c;在于他所拥有的。所以可以不学无术&#xff0c;但不能一无所有&#xff01; 技术领域&#xff1a;WEB安全、网络攻防 关注WEB安全、网络攻防。…

mysql 索引原理和使用

一、索引是什么&#xff1f; 1.1. 索引是什么 当一张表有 500 万条数据&#xff0c;在没有索引的 name 字段上执行一个查询&#xff1a; select * from user_innodb where name ‘jim’; 如果 name 字段上面有索引呢&#xff1f; ALTER TABLE user_innodb DROP INDEX idx_n…

快学会这个技能-.NET API拦截技法

大家好&#xff0c;我是沙漠尽头的狼。 本文先抛出以下问题&#xff0c;请在文中寻找答案&#xff0c;可在评论区回答&#xff1a; 什么是API拦截&#xff1f;一个方法被很多地方调用&#xff0c;怎么在不修改这个方法源码情况下&#xff0c;记录这个方法调用的前后时间&…

Java零基础入门到精通(持续更新中)

打开CMD命令窗口 WINR输入cmd 常用cmd命令代码 切换磁盘 E: 回车即可切换到e盘查看当前路径下的所有内容 dir进入目录 cd test回退到上一级目录 cd..进入多级目录 cd test\index\aaa回退到磁盘目录 cd \清屏 cls关闭命令行窗口 exit小例子&#xff1a;使用命令行窗口…

细粒度视觉分析综述TPAMI2021

细粒度图像分析&#xff08;FGIA&#xff0c;Fine-grained image analysis&#xff09;是计算机视觉中一个长期存在的基本问题&#xff0c;并支撑着一系列不同的现实应用。FGIA的任务目标是分析从属类别&#xff08;subordinate categories&#xff09;的视觉对象&#xff0c;例…

【Azure 架构师学习笔记】-Azure Data Factory (1)-调度入门

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Data Factory】系列。 前言 在开发好一个ADF pipeline&#xff08;功能&#xff09;之后&#xff0c;需要将其按需要运行起来&#xff0c;这个称之为调度。下图是一个简单的ADF 运作图&#xff0c; 按照需要的顺序&am…

uniapp 原生安卓开发插件(module),以及android环境本地调试(二)

uniapp 原生安卓开发插件&#xff08;module&#xff09;&#xff0c;以及android环境本地调试&#xff08;一&#xff09; 1、前景 承接上一篇文章&#xff0c;由于uniapp每天只有限定的打包次数&#xff0c;所以每次插件调试都打包成为基座&#xff0c;这个不太方便&#x…

java 集合常见面试(一)

集合概述 java集合预览 Java 集合&#xff0c; 也叫作容器&#xff0c;主要是由两大接口派生而来&#xff1a;一个是 Collection接口&#xff0c;主要用于存放单一元素&#xff1b;另一个是 Map 接口&#xff0c;主要用于存放键值对。对于Collection 接口&#xff0c;下面又有…

预告|第四届OpenI/O启智开发者大会NLP大模型论坛强势来袭!

最近&#xff0c;ChatGPT刷爆了所有人的朋友圈。它不仅能够与人类进行日常自然的聊天&#xff0c;还能胜任如写论文、编代码等诸多较为复杂的语言工作。ChatGPT 爆火的背后&#xff0c;是NLP(自然语言处理)技术的飞速革新。在过去的十年里&#xff0c;人工神经网络计算的加入、…

#461 年轻人的世界没有容易二字,除了脱发

点击文末“阅读原文”即可收听本期节目剪辑、音频 / 卷圈 编辑 / SandLiu 卷圈 监制 / 姝琦 文案 / 粒粒 产品统筹 / bobo 录音间 / 声湃轩提起二月二&#xff0c;你一定会脱口而出“龙抬头”。龙抬头吃什么很重要&#xff0c;重要到可以吵一架&#xff0c;但比吃什么更重要…

echo和swagger的结合使用(oapi-codegen使用)

echo和swagger的结合使用&#xff08;oapi-codegen使用&#xff09; 相关官网&#xff1a; echo官网swagger 这里介绍的重点是swagger和echo的整合使用&#xff0c;具体的框架的使用方法请看官方文档。 1. 初衷 swagger官网提供了文档转代码的操作&#xff0c;但转出来的代…

Allegro如何通过报表的方式检查单板上是否有假器件操作指导

Allegro如何通过报表的方式检查单板上是否有假器件操作指导 在做PCB设计的时候,输出生产文件之前,必须保证PCB上不能存在假器件,如下图,是不被允许的 当PCB单板比较大,如何通过报表的方式检查是否存在假器件,具体操作如下 点击Tools点击Reports

你看,ChatGPT都知道优先使用BigDecimal

不是三婶儿偏执&#xff0c;非要吐槽。家人们&#xff0c;咱就是说&#xff0c;按照基操逻辑谁会把严格金额计算相关的数据使用double类型呢… “我以为吕布已经够勇猛了&#xff0c;这是谁的部下&#xff1f;” 前几天&#xff0c;一同事让帮忙写段代码。内容比较常规&#xf…