C++进阶 智能指针

本篇博客简介:介绍C++中的智能指针

智能指针

  • 为什么会存在智能指针
    • 内存泄露
      • 内存泄漏定义
      • 内存泄漏的危害
      • 如何检测内存泄漏
      • 如何避免内存泄漏
  • 智能指针的使用及其原理
    • RAII
    • 设计一个智能指针
    • C++官方的智能指针
  • 定制删除器
  • 智能指针总结

为什么会存在智能指针

我们首先来看下面的这段代码

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;cout << "delete success!" << endl;
}int main()
{try{func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

在上面这段代码中有着一个很明显的内存泄露风险

当我们的程序运行在Func函数内的div函数时 很可能因为除0错误而跳转到另外一个执行流从而导致Func函数内两个new出来的内存没法被回收

为了解决这个问题我们发明了内存指针

内存泄露

内存泄漏定义

通常是由于我们的疏忽或者是程序错误导致未使用的内存没有被及时释放

这里有个经典的面试题 内存泄漏是内存丢了还是指针丢了

答案是指针丢了 因为我们能够找到指针就能够释放内存

内存泄漏的危害

内存泄漏会导致运行环境越来越慢 最终导致服务器崩溃

如何检测内存泄漏

Linux检测 : Linux内存泄漏检测工具

windows检测: Windows下内存泄漏检测工具

如何避免内存泄漏

  • 良好的编程习惯 主动申请的资源记得要主动释放
  • 利用RAII思想或智能指针来管理资源
  • 有些公司内部规范使用内部实现的私有内存管理库 这套库自带内存泄漏检测的功能选项
  • 出问题了使用内存泄漏工具检测

智能指针的使用及其原理

RAII

RAII的英文全称是 Resource Acquisition Is Initialization 直译过来即为 资源请求后初始化

它是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术

在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

设计一个智能指针

我们将上面的代码放在Linux平台下编译运行 能够得到这的结果

在这里插入图片描述
我们发现 没有除0错误的时候能正常delete掉new出来的空间

可是一旦发生了除0错误就会造成内存泄漏

为了防止这种情况 我们结合上面的RAII技术自己写出一个智能指针出来

template<class T>      
class SmartPtr      
{      private:      T* _ptr;      public:      SmartPtr(T* ptr)      :_ptr(ptr)      {}      ~SmartPtr()      {      delete _ptr;cout << "delete success!" << endl;                                                 }           
}; 

之后将源代码中的指针使用智能指针管理起来后重新编译运行

在这里插入图片描述
此时我们就会发现 不管有没有发生除0错误 new出来的内存都会被delete

为了让定义出来的智能指针对象更加符合原生指针的操作 我们使用operator操作符重载下 *->

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

C++官方的智能指针

这里介绍一个C++98版本中就有的指针指针 auto_ptr

它的头文件是memory

演示代码如下

  #include <iostream>    using namespace std;    #include <memory>    class A    {    public:    ~A()    {    cout << "delete A" << endl;    }    };    int main()    {    
W>  auto_ptr<A> ap1(new A);                                     return 0;    }   

编译运行之后我们可以发现 即使我们没有主动析构 它也自动帮我们调用了析构函数

(这里报警告的原因是auto_otr并不安全 实际上std::auto_ptr 已经在 C++11 中被弃用 并且在C++11中被删除 )

在这里插入图片描述
实际上auto_ptr能够做到的事情我们自己写的SmartPtr一样可以做到

而智能指针的难点也并不在这里 而在拷贝

如果我们写出这样子的代码

  SmartPtr<A> sp1(new A);    SmartPtr<A> sp2(sp1);   

那么编译运行之后就会出现双重释放问题

在这里插入图片描述
为什么会出现这样子的现象呢?

如下图
在这里插入图片描述
本来是只有一个sp1对象管理着一份资源

然后我们使用拷贝构造构造出了第二个对象sp2 由于我们没有写构造函数 所以说类使用默认构造函数浅拷贝同样指向了sp1的资源

那么此时两个对象同时管理同一份资源 当析构的时候自然会析构两次 自然就会出现上面的双重释放的错误了

那么我们应该如何解决这个错误呢?

方案一: 写一个深拷贝

这个方案虽然理论上可行 但是实际上它严重违背了我们使用智能指针的初衷 我们当初使用智能指针的目的就是为了管理资源 而如果使用了这个方案则进行拷贝构造的时候还会额外的占用资源 未免太得不偿失了

方案二: 管理权转移

auto_ptr使用的就是该方案

它的具体思路就是 将被拷贝对象管理的指针置空 将原来的指针拷贝到拷贝后的对象中

这是一种很不负责任的做法 因为如果使用了该方法 我们就极有可能遇到空指针的问题 实际上也就是因为这点auto_ptr在C++11以后被弃用

auto_ptr的赋值运算符重载思路

假设现在智能指针ap1管理着一个资源 指针指针ap2管理一个资源

进行了 ap1 = ap2 操作之后

ap1改为管理ap2的资源 ap1之前的资源会被释放掉 ap2的指针置空

当然 这是一个很差的设计思路 我们学习这个东西的意义仅仅在于了解 大家做项目的时候不要去使用这种思路

方案三:禁用拷贝

在C++11中的 unique_ptr就是使用的这种方案

实现方式也很简单

在C++11之后的版本 在构造函数后面加上 =delete 就可以

在C++11之前的版本 我们需要将拷贝构造函数和赋值函数只声明不实现并且私有化

方案四:引用计数

shared_ptr就是使用的这个方案

设计方案如图

在这里插入图片描述

我们每次创建一个对象就在计数器中加上一个数字 每次删除一个对象就在计数器中减去一个数字

直到计数器中的数字为0时 我们才真正的删除资源

那么我们如何定义这个计数器呢? 使用静态变量嘛?

使用静态变量肯定是不可以的 因为静态变量是一个全局变量 它虽然能解决多个对象管理一个资源的问题 但是却解决不了多个对象管理多个资源的问题

我们这里的解决方案应该是使用一个int类型的指针

当我们创建对象的时候给这个指针new出来一块空间作为计数器

每次拷贝的时候将这个int类型的指针也同样赋值 之后让计数器++即可

shared_ptr如何实现赋值运算符重载

shared_ptr的赋值运算符重载跟其他智能指针不同的一点是 它是多个对象共同管理者一个资源的

所以说我们赋值后不能简单的置空 还要考虑–计数器 如果–之后计数器为0 则还要考虑释放资源的问题

并且还要注意下一份资源不能给相同资源赋值的问题 (判断指向资源的指针是否相等即可)

循环引用问题

假如说我们现在用智能指针管理两个节点

在这里插入图片描述
现在自动释放还没有问题

可是如果我们做出下面两步操作 就会造成一个循环引用从而无法释放的问题

  1. 我们让n1的_next节点指向n2
  2. 我们让n2的_prev节点指向n1

在这里插入图片描述
到函数最后会按照定义的先后顺序反向析构 假设我们先定义的n1 后定义的n2 就会先析构n2 再析构n1

可以析构之后我们会发现这样子的场景

在这里插入图片描述

析构一次n2之后 由于计数器不为0 所以说n2资源依旧存在

析构一次n1之后 由于计数器不为0 所以说n1资源依旧存在

而由于n1的资源由n2的_prev指针管理
n2的资源由n1的_next指针管理

所以说

要想析构n1 首先要析构掉n2

而要想析构n2 首先要析构掉n1

这样子就形成了一个死循环 这个就是shared_ptr的循环引用问题 这个问题内部没有解决方式

为了解决这个问题 C++11发明了weak_ptr用来解决 shared_ptr的循环引用问题

我们可以把weak_ptr理解为shared_ptr的小跟班 它不单独出现

在节点里面的智能指针我们可以使用weak_ptr来进行定义

weak_ptr不会增加引用计数 但是可以正常的访问修改资源 从而也就不会存在循环引用问题了

代码表示如下

	template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get(){return _ptr;}private:T* _ptr; //管理的资源};

定制删除器

我们在上面试验的代码全部都是new的单个元素 在这种环境下没有析构没有暴露出问题

可以一旦我们使用 new [] 情况就复杂起来了 如下图

在这里插入图片描述

假设A类定义出来的对象大小为20个字节 new五个对象 那么我们实际开辟的空间为64字节 前面四个字节会存放着我们开辟了对象的个数 (int类型存放)

那么此时我们就不能简单的调用delete了 我们还要考虑指针偏移的问题

这个时候就到我们的定制删除器上场了

其实呢 定制删除器的写法很简单

我们只需要在模板处加上这行代码

  template<class T ,class D> 

删除处加上这两行代码就可以

        D del;    del(_ptr);

不过这样子写有个小问题 就是以后的shared_ptr就必须要传入两个参数了

当然这个问题也可以解决 我们给他设置一个默认的模板参数 delete即可

template<class T>                                                                   
struct DELETE                                                                       
{                                                                                   public:                                                                           void operator()(T* ptr)                                                         {                                                                               delete ptr;                                                                   }                                                                               
};                                                                                  template<class T ,class D = DELETE<T>>  

智能指针总结

为什么需要智能指针?

因为可能忘记释放资源造成内存泄漏

加上异常安全的原因 防不胜防

RAII机制是什么

英文是 Resource Acquisition Is Initialization 直译过来即为 资源请求后初始化

它是一种利用对象管理资源的思路 实际上将管理的责任托管给了对象

这种做法有两个好处

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针的发展历史

auto_ptr 到 bosst库中的三个智能指针 再到C++11中的三个智能智能

auto_ptr 在C++11被弃用 在C++17被彻底废除

auto_ptr unique_ptr shared_ptr weak_ptr的区别

前三个智能指针在RAII和模拟指针行为方面区别不大 主要区别在于拷贝方式

auto_ptr是一种不负责任的管理权转移

unique_ptr是简单粗暴的不准拷贝

shared_ptr则是引用计数

weak_ptr是shared_ptr的小跟班 来解决shared_ptr循环引用的问题

模拟实现一个智能指针

如果没有特殊要求我们优先实现unique_ptr 因为比较简单

如果有特殊要求那么一般就是实现shared_ptr了

这里比较难的主要是拷贝构造和赋值运算符重载的实现 下面给出实现代码

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

赋值运算符重载的注意点比较多

首先不能是自己给自己赋值 其次要想到赋值后原资源有没有消失

最后赋值的资源记得++

   SmartPtr& operator=(const SmartPtr<T>& sp){        if (_ptr == sp._ptr)                                                                                            {return *this;}if (--(*_pcount) == 0){delete _ptr;delete _pcount; }_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;return *this;}

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

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

相关文章

Spring5 AOP 默认使用 JDK

这是博主在使用dubbo实现远程过程调用的时候遇到的问题&#xff1a; 我们如果在服务提供者类上加入Transactional事务控制注解后&#xff0c;服务就发布不成功了。原因是事务控制的底层原理是为服务提供者类创建代理对象&#xff0c;而默认情况下Spring是基于JDK动态代理方式创…

SpringBoot 整合Swagger2

一、Swagger简介 Swagger是一套开源工具和规范&#xff0c;用于设计、构建和文档化RESTful Web服务。它允许开发人员定义API的各个方面&#xff0c;并生成易于理解的API文档和交互式API探索界面。同时&#xff0c;Swagger还提供代码生成工具&#xff0c;可自动生成与API交互的客…

MySQL和钉钉单据接口对接

MySQL和钉钉单据接口对接 数据源系统:钉钉 钉钉&#xff08;DingTalk&#xff09;是阿里巴巴集团打造的企业级智能移动办公平台&#xff0c;是数字经济时代的企业组织协同办公和应用开发平台。钉钉将IM即时沟通、钉钉文档、钉闪会、钉盘、Teambition、OA审批、智能人事、钉工牌…

dingding机器人

“自定义机器人”只支持消息发送&#xff0c;自动回复需要“企业内部机器人” 消息发送 import requests import jsonres requests.post(https://oapi.dingtalk.com/robot/send?access_token036a339axxx,data json.dumps({"text": {"content":"h…

医疗保健中的 NLP:实体链接

一、说明 HEalthcare和生命科学行业产生大量数据&#xff0c;这些数据是由合规性和监管要求&#xff0c;记录保存&#xff0c;研究论文等驱动的。但随着数据量的增加&#xff0c;搜索用于研究目的的必要文件和文章以及数据结构成为一个更加复杂和耗时的过程。例如&#xff0c;如…

消息队列(11) - 通信协议的设计

目录 通信协议设计代码实现 通信协议设计 对于我们客户端与服务器之间的通信协议我们约定如下&#xff1a; 具体的协议设计: 之后我们传递的参数也是这些 关于 type其实是在描述当前这个请求 、 响应是在调用那个API 约定如下 对于channel ,是tcp链接中的一个逻辑上的链接,…

策略模式实战应用

场景 假设做了个卖课网站&#xff0c;会员等级分为月vip、年vip、终生vip&#xff0c;每个等级买课的优惠力度不一样&#xff0c;传统的写法肯定是一堆的 if-else&#xff0c;现在使用策略模式写出代码实现 代码实现 策略模式的核心思想就是对扩展开放&#xff0c;对修改关闭…

应用案例|基于三维机器视觉的机器人纸箱拆码垛应用解决方案

Part.1 项目背景 在现代物流和制造行业中&#xff0c;纸箱的拆码垛操作是一项重要且频繁的任务。传统的纸箱拆码垛工作通常由人工完成&#xff0c;这种方式存在劳动强度大、生产效率低以及人为操作容易导致错误等问题&#xff0c;严重影响物料的安全运输和质量。为了满足物流行…

大模型“瘦身”进手机 下一个iPhone时刻将至?

一股“端侧大模型”浪潮正在涌来。华为、高通等芯片巨头正探索将AI大模型植入端侧&#xff0c;让手机实现新一代物种进化。 相比ChatGPT、Midjourney等AI应用依赖云端服务器提供服务&#xff0c;端侧大模型主打在本地实现智能化。它的优势在于能够更好地保护隐私&#xff0c;同…

有没有推荐的golang的练手项目?

前言 下面是github上的golang项目&#xff0c;适合练手&#xff0c;可以自己选择一些项目去练习&#xff0c;整理不易&#xff0c;希望能多多点赞收藏一下&#xff01;废话少说&#xff0c;我们直接进入正题>>> 先推荐几个教程性质的项目&#xff08;用于新手学习、巩…

RS-232标准

目录 1、概述2、RS-232接口的特点3、RS-232接口协议【仿真】 1、概述 RS-232接口是在1970年由美国电子工业协会(EIA)联合贝尔系统、调制解调器厂家及计算机终端生产厂家共同制定的用于串行通讯的标准。它的全名是“数据终端设备(DTE)和数据通讯设备(DCE)之间串行二进制数据交换…

Hadoop理论及实践-HDFS读写数据流程(参考Hadoop官网)

NameNode与DataNode回顾 主节点和副本节点通常指的是Hadoop分布式文件系统&#xff08;HDFS&#xff09;中的NameNode和DataNode。 NameNode&#xff08;主节点&#xff09;&#xff1a;NameNode是Hadoop集群中的一个核心组件&#xff0c;它负责管理文件系统的命名空间和元数据…

arcgis pro 3.0.2 安装及 geemap

arcgis pro 3.0.2 安装及 geemap arcgis pro 3.0.2 安装 arcgis pro 3 版本已经很多了&#xff0c;在网上找到资源就可以进行安装 需要注意的是&#xff1a;有的文件破解文件缺少&#xff0c;导致破解不成功。 能够新建地图就是成功了&#xff01; geemap安装 1.需要进行环…

Python web实战之Django 的 WebSocket 支持详解

关键词&#xff1a;Python, Django, WebSocket, Web 如何使用 Django 实现 WebSocket 功能&#xff1f;本文将详细介绍 WebSocket 的概念、Django 的 WebSocket 支持以及如何利用它来创建动态、响应式的 Web 应用。 1. WebSocket 简介 1.1 什么是 WebSocket&#xff1f; 在 W…

【果树农药喷洒机器人】Part7:静态PWM变量喷药实验

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

【编程指南】ES2016到ES2023新特性解析一网打尽

ES2016 Array.prototype.includes() Array.prototype.includes 方法&#xff1a; 这个方法用于检查数组是否包含特定元素&#xff0c;如果包含则返回 true&#xff0c;否则返回 false // 我有一个水果篮子 const fruitBasket [apple, banana, orange, grape];// 我要检查篮…

pycharm配置conda虚拟环境

&#x1f4d5;作者简介&#xff1a;热编程的贝贝&#xff0c;致力于C/C、Java、Python等多编程语言&#xff0c;热爱跑步健身&#xff0c;喜爱音乐的一位博主。 &#x1f4d7;本文收录于贝贝的日常汇报系列&#xff0c;大家有兴趣的可以看一看 &#x1f4d8;相关专栏深度学习、…

iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能

iOS开发-实现二维码扫一扫Scan及识别图片中二维码功能 在iOS开发中&#xff0c;会遇到扫一扫功能&#xff0c;扫一扫是使用摄像头扫码二维码或者条形码&#xff0c;获取对应二维码或条形码内容字符串。通过获得的字符串进行跳转或者打开某个页面开启下一步的业务逻辑。 https…

RFID工业识别技术:供应链智能化的科技颠覆

RFID工业识别技术&#xff0c;作为物联网的先锋&#xff0c;正在供应链管理领域展现着前所未有的科技颠覆。从物料追踪到库存管理&#xff0c;再到物流配送&#xff0c;RFID技术以其高效的数据采集和智能的自动化处理&#xff0c;彻底改变着传统供应链的运营方式。 RFID在物料追…

Linux学习————redis服务

目录 一、redis主从服务 一、redis主从服务概念 二、redis主从服务作用 三、缺点 四、主从复制流程 五、搭建主从服务 配置基础环境 下载epel源&#xff0c;下载redis​编辑 二、哨兵模式 一、概念 二、作用 三、缺点 四、结构 五、搭建 修改哨兵配置文件 启动服务…