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.rhkb.cn/news/82283.html

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

相关文章

Linux常见命令

新建标签页 (gitee.com)尹相辉 (yinxianghui66) - Gitee.com新建标签页 (gitee.com) 文章目录 文章目录 一、Linux常见命令 1.ls 2.cd 目录名 3.pwd 4.touch 文件名 5.echo 字符串->目标文件 6.cat 文件名 7.man 8.vim 文件名 9.mkdir 目录名 10.rm 文件名 11.mv 源…

WEB集群——负载均衡集群

目录 一、 LVS-DR 群集。 1、LVS-DR工作原理 2、LVS-DR模式的特点 3、部署LVS-DR集群 3.1 配置负载调度器&#xff08;192.168.186.100&#xff09; 3.2 第一台web节点服务器&#xff08;192.168.186.103&#xff09; 3.3 第二台web节点服务器&#xff08;192.168.186.…

tui.calender日历在vue中的使用1.0

官网&#xff1a;https://ui.toast.com/tui-calendar github&#xff1a;https://github.com/nhn/tui.calendar/tree/main 月、周、日视图都有&#xff0c;拖拽也比较方便&#xff0c;但是自己用起来比较费劲&#xff0c;参考文档写得不全&#xff0c;做个记录日后方便参考&…

Freemarker:生成HTML文本文件

前置工作参考&#xff1a; Freemarker&#xff1a;基本使用_moreCalm的博客-CSDN博客 1、修改application.yml配置文件 server:port: 8881 #服务端口 spring:application:name: freemarker-demo #指定服务名freemarker:cache: false #关闭模板缓存&#xff0c;方便测试settin…

数据结构----c语言复习

数据结构----c语言复习 一.类型 1.类型的种类 char 1个字节 范围-128~127 short 2个字节 范围-32768~32767 int 4个字节 范围-2147483648~2147483647 long 4个字节 范围-2147483648~2147483647 float 4个字节 有效位为6~7位 float 8个字节 有效位为15~16为 unsigned c…

Jmeter添加cookie的两种方式

jmeter中添加cookie可以通过配置HTTP Cookie Manager&#xff0c;也可以通过HTTP Header Manager&#xff0c;因为cookie是放在头文件里发送的。 实例&#xff1a;博客园点击添加新随笔 https://i.cnblogs.com/EditPosts.aspx?opt1 如果未登录&#xff0c;跳转登录页&#xf…

空地协同智能消防系统——无人机、小车协同

1 题目 1.1 任务 设计一个由四旋翼无人机及消防车构成的空地协同智能消防系统。无人机上安装垂直向下的激光笔&#xff0c;用于指示巡逻航迹。巡防区域为40dm48dm。无人机巡逻时可覆盖地面8dm宽度区域。以缩短完成全覆盖巡逻时间为原则&#xff0c;无人机按照规划航线巡逻。发…

【elementui】解决el-select组件失去焦点blur事件每次获取的是上一次选中值的问题

目录 【问题描述】 【问题摘要】 【分析问题】 【完整Test代码】 【封装自定义指令】 ↑↑↑↑↑↑↑↑↑↑↑↑ 不想看解决问题过程的可点击上方【封装自定义指令】目录直接跳转获取结果即可~~~ 【问题描述】 一位朋友遇到这么一个开发场景&#xff1a;在表格里面嵌入el-…

Hololens2二维码识别

配置 目前大部分Hololens进行二维码识别的开发都是基于ZXing的包完成&#xff0c;首先需要完成zxing.unity.dll&#xff0c;很多地方应该都能下载&#xff0c;也可以直接上github上下载&#xff08;下载点这里&#xff09;。 下载时注意一下版本就好&#xff0c;过老的zxing兼…

2023-08-02 LeetCode每日一题(翻转卡片游戏)

2023-08-02每日一题 一、题目编号 822. 翻转卡片游戏二、题目链接 点击跳转到题目位置 三、题目描述 在桌子上有 N 张卡片&#xff0c;每张卡片的正面和背面都写着一个正数&#xff08;正面与背面上的数有可能不一样&#xff09;。 我们可以先翻转任意张卡片&#xff0c;…

一位年薪40W的测试被开除,回怼的一番话,令人沉思

一位年薪40W测试工程师被开除回怼道&#xff1a;“反正我有技术&#xff0c;在哪不一样” 一技傍身&#xff0c;万事不愁&#xff0c;当我们掌握了一技之长后&#xff0c;在职场上说话就硬气了许多&#xff0c;不用担心被炒&#xff0c;反过来还可以炒了老板&#xff0c;这一点…

解决 Android Studio 的 Gradle 面板上只有关于测试的 task 的问题

文章目录 问题描述解决办法 笔者出问题时的运行环境&#xff1a; Android Studio Flamingo | 2022.2.1 Android SDK 33 Gradle 8.0.1 JDK 17 问题描述 笔者最近发现一个奇怪的事情。笔者的 Android Studio 的 Gradle 面板上居然除了用于测试的 task 之外&#xff0c;其它什…

Java中String方法魔性学习

这里写目录标题 先进行专栏介绍String详解常用构造方法代码演示常用成员方法代码示例总结 先进行专栏介绍 本专栏是自己学Java的旅途&#xff0c;纯手敲的代码&#xff0c;自己跟着黑马课程学习的&#xff0c;并加入一些自己的理解&#xff0c;对代码和笔记 进行适当修改。希望…

机器学习基础知识(1)

什么是机器学习 机器学习是一种通过输入大量数据来构建一种模型&#xff08;网络&#xff09;&#xff0c;这个训练好的模型将会被用来预测或执行某些操作&#xff0c;这个训练的过程和方法就是机器学习。 我们也可以理解为构建一个“函数”&#xff0c;使得这个函数面对我们…

【第一阶段】kotlin的range表达式

range:范围&#xff1a;从哪里到哪里的意思 in:表示在 !in&#xff1a;表示不在 … :表示range表达式 代码示例&#xff1a; fun main() {var num:Int20if(num in 0..9){println("差劲")}else if(num in 10..59){println("不及格")}else if(num in 60..89…

神码ai伪原创【php源码】

大家好&#xff0c;小编为大家解答python必备常用英语词汇笔记的问题。很多人还不知道python中常用的英语单词&#xff0c;现在让我们一起来看看吧&#xff01; 火车头采集ai伪原创插件截图&#xff1a; 一.什么是注释 注释是对一段代码的解释&#xff0c;不参与程序运行&…

岩土工程仪器多通道振弦传感器信号转换器应用于隧道安全监测

岩土工程仪器多通道振弦传感器信号转换器应用于隧道安全监测 多通道振弦传感器信号转换器VTI104_DIN 是轨道安装式振弦传感器信号转换器&#xff0c;可将振弦、温度传感器信号转换为 RS485 数字信号和模拟信号输出&#xff0c;方便的接入已有监测系统。 传感器状态 专用指示灯方…

怎么采集网址、图片地址等标签属性

如果只想采集网址、图片地址等HTML标签属性的值&#xff0c;要怎么采集呢&#xff1f; 可以使用简数采集器&#xff0c;有对应的快捷提取方式&#xff0c;一键即可转换为采集网址、采集图片地址方式&#xff0c;非常方便快捷。 也可以自行设置高级提取&#xff0c;提取Html标…

生活小妙招之UE custom Decal

因为这几年大部分时间都在搞美术&#xff0c;所以博客相关的可能会鸽的比较多&#xff0c;阿巴阿巴 https://twitter.com/Tuatara_Games/status/1674034744084905986 之前正好看到一个贴花相关的小技巧&#xff0c;正好做一个记录&#xff0c;也在这个的基础上做一些小的拓展…

REDIS哨兵模式

目录 前言 一、哨兵模式概念 二、作用 三、缺点 四、结构 五、搭建 总结 前言 Redis哨兵模式是一种用于实现Redis高可用性的机制。在哨兵模式下&#xff0c;有一个或多个哨兵进程监控Redis主节点和从节点的状态&#xff0c;并在主节点出现故障时自动将一个从节点升级为新的主节…