yo!这里是智能指针相关介绍

目录

前言

内存泄漏

RAII

智能指针原理

智能指针分类

auto_ptr

unique_ptr

shared_ptr

两个问题

线程安全

循环引用

后记


前言

        对于智能指针,听起来很高大上,其实本质上就是一个类。为什么叫指针呢?因为可以像指针一样管理一块资源;为什么又加上智能两个字呢?因为比普通指针更特殊一点,特殊在它是一个类。比如说,当我们使用一个指针去new一块空间时,最后要delete去释放资源,在不注意时就会忘记导致内存泄漏,或者因为像上一章节中throw这样会跳跃的语句,当跳过delete又该怎么办呢?这就需要使用到RAII的思想,在下面将会介绍到,在此之前我们先介绍一下内存泄漏的危害,以此来强调一下智能指针的重要性。

内存泄漏

        内存泄漏通常发生在程序中有未释放的内存空间,导致程序的内存占用不断增加。这可能是由于以下原因之一导致的:

  1. 程序中使用了动态内存分配函数(如malloc、calloc等)分配的内存,但在使用完后未使用free等函数释放内存;

  2. 程序中存在指针错误,导致内存无法访问或释放;

  3. 程序中存在死循环或递归,导致一些内存空间无法被释放;

  4. 程序中存在逻辑错误,导致内存未能被正确地分配或释放,

        内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费,导致响应越来越慢,最终卡死。

eg:

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

 如何避免内存泄漏

        ①养成好的编程习惯,对容易造成内存泄漏的语句产生敏感,比如new出来的空间紧接着释放,然后在在中间写使用此空间的逻辑,若涉及异常安全问题,应注意在new的空间所在的栈帧捕获拦截一下,将相应资源释放再处理异常;

        ②使用提前实现的私有内存管理库,自带内存泄漏检测的功能,比如内存池;

        ③使用内存泄漏检测工具,但这类工具不靠谱且收费;

        ④采用智能指针或其思想——RAII来管理资源,下面介绍。

RAII

        RAII 是 Resource Acquisition Is Initialization 的缩写,即资源获取即初始化。用于管理资源(如内存、文件句柄、网络连接等)的生命周期。RAII 基本思想是将资源的创建和释放绑定在一个对象的构造和析构过程中,通过对象的生命周期来管理资源

        在使用 RAII 时,需要创建一个对象来管理资源,这个对象在构造函数中获取资源,然后在析构函数中释放资源。这样一来,无论是因为异常还是正常的程序流程终止,都可以确保资源得以释放,不会导致内存泄漏或资源占用问题。

优点:

  1. 保证资源分配和释放的可靠性,减少了程序错误的风险;
  2. 使代码更加简洁、易于维护和阅读;
  3. 避免了程序员必须手动跟踪资源分配和释放,从而减少了程序员的工作量,

        RAII 是面向对象设计和编程的一种实践,在 C++ 中得到了广泛的应用。例如,在 C++ 标准库中就使用了 RAII 的技术实现了自动内存管理、文件操作和线程同步等功能。

eg:

template <class T>
class PTR
{
public:PTR(T* ptr = nullptr):_ptr(ptr){}~PTR(){if (_ptr)delete _ptr;}
private:T* _ptr = nullptr;
};int main()
{PTR<int> p1(new int);PTR<int> p2(new int[10]);return 0;
}

智能指针原理

        前面说过,智能指针本质就是一个,虽应用了RAII思想还不够,还需要具备普通指针的功能,也就是解引用和->访问空间中的内容,重载操作符即可。

eg:

template <class T>
class Smartptr
{
public:Smartptr(T* ptr = nullptr):_ptr(ptr){}~Smartptr(){if (_ptr)delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr = nullptr;
};

智能指针分类

  • auto_ptr

        在c++98,stl就提供了一版智能指针——auto_ptr,下面是一份模拟实现,框架与上面的Smartptr一样,在此基础上实现了拷贝构造函数和赋值运算符重载,可以看到,auto_ptr的实现原理在于资源管理权的转移

        对于拷贝构造函数,将被拷贝对象内部指针指向的资源赋给当前对象的内部指针,且将被拷贝对象内部指针指向空;对于赋值运算符重载,若当前对象内部指针有指向资源,那么直接将此资源释放,然后将被拷贝对象内部指针指向的资源赋给当前对象的内部指针,再将被拷贝对象内部指针指向空。

        可以从模拟实现看出,auto_ptr是一个较为失败的提出,有很多公司明确指出不能使用此智能指针,比如说,当一个智能指针对象赋值给另一个对象时,那么当前智能指针对象在后续就不能用了,因为已经不再管理那一块资源了。可想而知这不是我们想要的功能,我们想要的是在赋值给其他对象之后,自己依旧可以继续管理那块资源,与其他对象一块管理。

 模拟实现:

template <class T>
class Auto_ptr
{
public:Auto_ptr(T* ptr = nullptr):_ptr(ptr){}~Auto_ptr(){if (_ptr)delete _ptr;}Auto_ptr(Auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}Auto_ptr<T>& operator=(Auto_ptr<T>& ap){if (this != &ap){if (_ptr){delete _ptr;}_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr = nullptr;
};

test:

  • unique_ptr

        c++11中,stl又提出另一款智能指针——unique_ptr,实现原理也是简单粗暴,因为上面的auto_ptr在赋值以后被赋值指针在后续就不能继续管理那块资源了,所以unique_ptr直接禁止拷贝和赋值,也就是一个智能指针对象在构造时就决定了管理哪块资源,后续就不能变了,有新的资源需要被管理,只能重新构造一个对象,下面是unique_ptr的模拟实现。

模拟实现:

template <class T>
class Unique_ptr
{
public:Unique_ptr(T* ptr = nullptr):_ptr(ptr){}~Unique_ptr(){if (_ptr){delete _ptr;}}Unique_ptr(Unique_ptr<T>& up) = delete;Unique_ptr<T>& operator=(Unique_ptr<T>& up) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr = nullptr;
};

 test:

  • shared_ptr

        与上面两个智能指针不同,c++11中还有一款智能指针——shared_ptr,这款智能指针就如同我们想象的那样了,可以支持赋值拷贝,而且复制拷贝之后,原对象依旧可以管理那一块资源,与赋值对象一块管理。shared_ptr的原理在于通过引用计数的方式来实现多个对象之间共享资源,最后一个析构对象释放资源。具体地,在shared_ptr内部,每个资源都维护了着一份计数,用来记录该份资源被几个对象共享,在对象被销毁时(调用析构函数),就说明自己不使用该资源了,对象的引用计数减一,如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源,如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源。

        下面是模拟实现,除了Smartptr基本的框架外,成员属性还多个_pCount用来计数,本质是申请一块int大小的空间记录对应资源的被指向个数,两块资源一一对应,比如说,

这里使用一一对应的一块资源去引用计数就很巧妙,想一下为什么不用静态成员变量去计数?这个问题后面会说。

        继续,_pCount在构造函数中初始化为1,在析构函数中如果是最后一个指向资源的对象,则与资源一块被释放,否则将计数减一。最主要的还是拷贝构造函数和赋值运算符重载的实现,对于拷贝构造函数,将指向的资源与计数资源赋值给目标对象之后,将计数加一;对于赋值运算符重载,因为涉及到原对象资源的释放,所以较为复杂一点,首先根据原对象中的引用计数决定是否要释放原对象指向的资源还是计数减一,再指向被赋值对象的资源,将计数加一即可。

模拟实现:

template <class T>
class Shared_ptr
{
public:Shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)){}~Shared_ptr(){if (--(*_pCount) == 0){delete _ptr;delete _pCount;}}Shared_ptr(Shared_ptr<T>& sp){_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}Shared_ptr<T>& operator=(Shared_ptr<T>& sp){if (_ptr != sp._ptr)   //这里不建议用(this!=&sp),因为除了同一个对象不能进行赋值操作,//而且指向同一块资源的两个对象也不进行赋值运算{if (--(*_pCount) == 0){delete _ptr;delete _pCount;}_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr = nullptr;int* _pCount;
};

 test:

为什么不使用静态成员static int _count去计数呢?

        因为静态成员属于整个类,属于此类实例化出的所有对象,我们需要一个资源配对一个计数,要是使用静态成员计数,则所有资源都只有这一个计数,比如:

        想一下上面实现的shared_ptr还有啥问题不?当我们申请一个数组大小的资源或者使用malloc去申请资源时,类内实现的析构函数还能用吗?我们知道,删除数组资源需要使用delete[],删除使用malloc申请的资源需要用free释放,所以类内只是用delete释放资源是不全面的。这里我们可以通过在类模板参数中传入删除器的类决定释放资源的方式。在stl库里,shared_ptr的删除器是在构造时传入删除器对象,这种不太好实现,至少以现在的简单架构不好实现,所以采用在模板参数处传入删除器的方法,如下。

带删除器版本的shared_ptr模拟实现:

//默认
template <class T>
struct Delete
{void operator()(T* ptr){//cout << "delete" << endl;delete ptr;}
};
//删除数组申请的资源
template <class T>
struct deleteArray
{void operator()(T* ptr){cout << "delete[]" << endl;delete[] ptr;}
};
//删除malloc申请的资源
template <class T>
struct freeFunc
{void operator()(T* ptr){cout << "free" << endl;free(ptr);}
};template <class T, class D = Delete<T>>
class Shared_ptr
{
public:Shared_ptr(T* ptr = nullptr):_ptr(ptr), _pCount(new int(1)){}~Shared_ptr(){D del;if (--(*_pCount) == 0){del(_ptr);delete _pCount;}}Shared_ptr(Shared_ptr<T>& sp){_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}Shared_ptr<T>& operator=(Shared_ptr<T>& sp){D del;if (_ptr != sp._ptr)   //这里不建议用(this!=&sp),因为除了同一个对象不能进行赋值操作,//而且指向同一块资源的两个对象也不进行赋值运算{if (--(*_pCount) == 0){del(_ptr);delete _pCount;}_ptr = sp._ptr;_pCount = sp._pCount;(*_pCount)++;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr = nullptr;int* _pCount;
};

test:

两个问题

  • 线程安全

        后面写

  • 循环引用

        从上面的一系列智能指针可以看出,shared_ptr是当中最符合实际情况使用的一个,但是在后续使用的过程中,有人发现有些场景使用智能指针引发了意料之外的结果,导致这个结果的原因就是循环引用的问题,看下面这个例子(其中use_count函数是返回智能指针的引用计数):

struct Node
{~Node(){cout << "~node" << endl;  //无别的作用,只是看Node资源有无释放}int _data;shared_ptr<Node> _next;shared_ptr<Node> _prev;
};void testRotateRef()
{shared_ptr<Node> n1(new Node);shared_ptr<Node> n2(new Node);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;
}

         对于链表节点类,我们使用智能指针去管理资源,为什么我们类内的next、prev也使用智能指针定义呢?因为会存在赋值时有类型转换问题,比如n1->_next = n2;,那么问题就来了,我们先编译一下testRotateRef函数:

        先甭管引用计数的变化,我们可以看到n1、n2的Node资源并没有释放(也就是没有打印出~Node),这是怎么回事呢?截图中可以看出,n1、n2定义完以后的引用计数都是1很正常,经过n1->_next = n2;n2->_prev = n1;后,引用计数都变成了2也正常,因为对于n2的资源,多了个n1的next智能指针指向n2资源,对于n1资源也是一样,所以引用计数都是2很正常,但是当这些语句走完时,正常来说两个Node节点资源应该释放了,但是并没有,因为两个资源的引用计数都是2,无法得以释放,图解如下:

        上图可以看出,n1、n2在出作用域即将释放时,由于引用计数不是1,从而无法释放,导致这一现象的正是循环引用的问题,也就是两个资源互相牵制,使彼此无法正常释放。针对这个现象,c++11提出了weak_ptr的智能指针,捆绑shared_ptr以解决循环引用的问题,可看作shared_ptr的小跟班,可相互赋值,本质就是不参与资源的释放管理,因此不增加计数,但是可以访问修改资源。我们将Node类内的智能指针的类型改为weak_ptr再试一次:

        可以看到,指向前后的引用计数并没有变化,而且两个Node节点资源得以释放,这就解决了循环引用的问题。

        下面我们也模拟实现一下weak_ptr,如下代码块(其中get函数是返回智能指针的类内原生指针),除了Smart_ptr的基础框架,也除了基本的构造函数、拷贝构造函数、赋值运算符重载以外,类内多了个用shared_ptr类型构造的拷贝构造函数(也有用shared_ptr类型的赋值运算符重载,这里没写),这保证了weak_ptr和shared_ptr之间可以相互赋值,同时无析构函数,因为weak_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(const Weak_ptr<T>& wp):_ptr(wp._ptr){}Weak_ptr<T>& operator=(const Weak_ptr<T>& wp){_ptr = wp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};

后记

        智能指针是c++区别于其他语言独有的一个功能,但是并不是说其他语言没有前言中讲述的问题,只是有别的处理方法,比如java有垃圾处理器,能够将new的资源自动回收。智能指针本身不重要,重要的是RAII的思想,能将资源的管理交给一个类,这个方法就很符合面向类和对象的语言,并且智能指针的知识点在面试中被问到的频率非常高,比如手撕一个智能指针,RAII的思想是什么,针对于本文的最后两个问题中的提问点也有很多,这一章节不是特别难,希望大家可以深入学习一下,可以在面试中从容的面对任何提问。


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

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

相关文章

linux 应用开发笔记---【I/O文件/基础篇 】

文章笔记来自于【速学Linux】手把手教你学嵌入式Linux C应用编程_哔哩哔哩_bilibili 一&#xff0c;什么是linux应用程序 1.运行在linux操作系统用户空间的程序 2.内核程序运行在内核空间&#xff0c;应用程序运行在用户空间 在终端执行的命令ls,ps。。。。。。都是运行在用…

【MySQL的DQL查询语句】

MySQL的DQL查询语句-----在Navicat下 将学生表导入Navicat中查询语句查询一整张表查询年龄大于22年龄大于22的女生查找文科的学生查找六班的学生计算学生的总分 &#xff08;group by&#xff09;合并两表 &#xff08;join on xxxx&#xff09;合并两张表 并求总分先合并在聚合…

Java+springboot+avue医院绩效考核系统源码支持二次开发

公立医院改革要求建立公立医疗卫生机构绩效考核体系&#xff0c;借助绩效考核来引导各级公立医院把社会效益摆在首位&#xff0c;提高医疗服务质量&#xff0c;规范医疗服务行为&#xff0c;加强医院内部管理&#xff0c;促进医院高质量发展 医院绩效考核系统&#xff0c;建立以…

python 运用pandas 库处理excel 表格数据

文章目录 读取文件查看数据数据选择数据筛选创建新列计算并总结数据分组统计 读取文件 Pandas 是一个强大的数据分析库&#xff0c;它提供了丰富的数据结构和数据分析工具&#xff0c;其中之一是用于读取不同格式文件的 read_* 函数系列。以下是一个简单介绍如何使用 Pandas 读…

Siemens-NXUG二次开发-C/C++/Python环境配置[20231204]

Siemens-NXUG二次开发-C/C/Python运行方式[20231204] 1.NX/UG C/C/Python API官方开发文档2.运行方式2.1内部模式2.2 外部模式2.3 许可证书服务器启动 3.C/C环境配置4.Python环境配置5.第三方环境配置 1.NX/UG C/C/Python API官方开发文档 西门子NX/UG Python api开发文档&…

Spring学习笔记:Day2

昨天定的学习计划发现通过文心4.0来实现不靠谱&#xff0c;坑太多&#xff0c;今天开始跟随B站进行学习&#xff0c;争取10-15天学习一遍&#xff0c;冲啊&#xff01; 地址&#xff1a;001-课程介绍_哔哩哔哩_bilibili 今日规划&#xff1a; pt 001 - pt 018&#xff0c;提到…

小心处理 C++ 静态变量中的陷阱

小心处理 C 静态变量中的陷阱 函数中的 static 变量 static 变量的作用 C 中 static 关键字的最后一个用途是在函数内创建局部变量&#xff0c;这些变量在其作用域内退出和进入时保持其值。函数内的 static 变量类似于只能从该函数访问的全局变量。static 变量的一个常见用途…

【UGUI】实现背包的常用操作

1. 添加物品 首先&#xff0c;你需要一个包含物品信息的类&#xff0c;比如 InventoryItem&#xff1a; using UnityEngine;[CreateAssetMenu(fileName "NewInventoryItem", menuName "Inventory/Item")] public class InventoryItem : ScriptableObje…

网工学习7-配置 GVRP 协议

7.1GARP概述 GARP(Generic Attribute Registration Protocol)是通用属性注册协议的应用&#xff0c;提供 802.1Q 兼容的 VLAN 裁剪 VLAN pruning 功能和在 802.1Q 干线端口 trunk port 上建立动态 VLAN 的功能。 GARP 作为一个属性注册协议的载体&#xff0c;可以用来传播属性…

游泳馆会员服务预约管理系统预约小程序效果如何

游泳馆在各地每天都有大量用户前往&#xff0c;夏季室外、冬季室内也是学习游泳技术和休闲娱乐的好地方&#xff0c;而消费者大多是年轻人和家长带的孩子&#xff0c;这部分群体更显年轻化&#xff0c;因此在如今互联网环境下&#xff0c;传统商家需要进一步赋能客户消费路径。…

【Vue】Vue CLI 脚手架(Vue Command Line Interface)安装教程(通过npm)

前言 Vue CLI&#xff08;Vue Command Line Interface&#xff09;是一个基于Vue.js的官方脚手架工具&#xff0c;用于快速搭建和管理Vue.js项目。它提供了一套完整的开发工具和配置&#xff0c;包括项目初始化、开发服务器、热重载、构建和打包等功能。 Vue CLI使用了Webpac…

自动驾驶学习笔记(十三)——感知基础

#Apollo开发者# 学习课程的传送门如下&#xff0c;当您也准备学习自动驾驶时&#xff0c;可以和我一同前往&#xff1a; 《自动驾驶新人之旅》免费课程—> 传送门 《Apollo Beta宣讲和线下沙龙》免费报名—>传送门 文章目录 前言 传感器 测距原理 坐标系 标定 同…

2023/12/3总结

RabbitMq 消息队列 下载地址RabbitMQ: easy to use, flexible messaging and streaming — RabbitMQ 使用详情RabbitMQ使用教程(超详细)-CSDN博客 实现延迟队列&#xff08;为了实现订单15分钟后修改状态&#xff09; 1 死信队列 当一个队列中的消息满足下列情况之一时&…

【risc-v】易灵思efinix FPGA riscv 时钟配置的一些总结

系列文章目录 分享一些fpga内使用riscv软核的经验&#xff0c;共大家参考。后续内容比较多&#xff0c;会做成一个系列。 本系列会覆盖以下FPGA厂商 易灵思 efinix 赛灵思 xilinx 阿尔特拉 Altera 本文内容隶属于【易灵思efinix】系列。 文章目录 系列文章目录前言一、pan…

为何要3次握手?TCP协议的稳定性保障机制

&#x1f680; 作者主页&#xff1a; 有来技术 &#x1f525; 开源项目&#xff1a; youlai-mall &#x1f343; vue3-element-admin &#x1f343; youlai-boot &#x1f33a; 仓库主页&#xff1a; Gitee &#x1f4ab; Github &#x1f4ab; GitCode &#x1f496; 欢迎点赞…

操作系统·存储器管理

根据冯诺依曼原理&#xff0c;程序必须先存储在内存中&#xff0c;才可以执行。 在多道程序并发执行的系统存储器管理非常重要。 5.1 存储器管理的功能 5.1.1 主存分配与回收 要完成内存的分配和回收工作&#xff0c;要求设计者选择和确定几种策略和结构&#xff1a; 1.调入…

STM32F407-14.3.10-01PWM模式

PWM 模式 脉冲宽度调制模式可以生成一个信号&#xff0c;该信号频率由 TIMx_ARR⑩ 寄存器值决定&#xff0c;其占空比由 TIMx_CCRx⑤ 寄存器值决定。 通过向 TIMx_CCMRx 寄存器中的 OCxM⑰ 位写入 110 &#xff08;PWM 模式 1&#xff09;或 111 &#xff08;PWM 模式 2&#…

SpringCloud简介和用处

Spring Cloud是一套基于Spring Boot的微服务框架&#xff0c;它旨在提供一种快速构建分布式系统的方法。它可以帮助开发人员构建具有高可用性、可扩展性和容错性的微服务&#xff0c;并通过Spring Boot的开发工具和库提供强大的支持。 一、简介 Spring Cloud是Spring家族中的一…

React如何像Vue一样将css和js写在同一文件

如果想在React中想要像Vue一样把css和js写到一个文件中&#xff0c;可以使用CSS-in-JS。 使用CSS-in-JS 下载 npm i styled-components使用 就像写scss一样&#xff0c;不过需要声明元素的类型 基本语法及展示如下&#xff0c; import styled from "styled-component…

周周清(1)

项目进度&#xff1a; 最近一直在搭建环境&#xff0c;都没写什么&#xff1a;登陆页面采用登陆注册在同一个界面&#xff0c;用v-if进行渲染&#xff0c;并且借助validation插件中的yup神器进行校验&#xff0c; <script setup> // import { ref } from vue import * …