C++进阶 智能指针

2023-11-18

本篇博客简介:介绍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;
    }

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++进阶 智能指针 的相关文章

随机推荐

  • VUE3 + TS + MapboxGL + 拖拽定位

    VUE3 TS MapboxGL 拖拽定位 mapbox的拖拽定位 相对于其他api来说还是相对容易实现的 只需要在中心创建图层 之后在地图移动时候一直更改图层的中心即可 首先 安装部分不过多描述 官网安装地址 之后 要使用mapboxGL
  • Linux启动网卡时出现RTNETLINK answers: File exists错误解决方法

    一 问题描述 VMware中克隆虚拟机是经常的事情 虽然如此 用到虚拟机时 本人还是喜欢新安装一个操作系统 针对服务器的应用 在安装操作系统时 一并安装好 并且也花不了多少时间 但最近需要大量的配置一样的虚拟机进行测试 故安装了一个模板虚拟
  • 几种基本放大电路详解

    可提前了解的文章 运算放大器 运放 介绍 注意 此处我们采用的是Multisim软件仿真 链接中有详细安装教程 注意 仿真只是数学运算 实际情况的话 就不是数学运算那么简单 有很多复杂的因数在里面 所以具体情况要参照实际电路搭建 比如说 之
  • C++模板-33-类模板和函数模板的区别

    这篇开始学习类模板相关知识 本篇主要学习什么是类模板 还有类模板和函数模板的区别 1 类模板语法 template
  • Java调试的变迁:从System.out.println到log4j

    jungleford如是说 用惯了VC的人刚接触Java大概很不习惯代码的调试 的确 在M 的大部分IDE都做得相当出色 包括像VJ 这样一直被Java程序员称为是 垃圾 的类库 记得以前在瀚海星云的Java版提有关VJ问题的人是有可能被封
  • linux下python第三库(setuptools)的安装

    rpm源的下载网址 http rpm pbone net http www rpmfind net linux RPM index html python第三方模块的下载网址 https pypi python org pypi setup
  • amd 皓龙 服务器 芯片,全面解读 关于AMD皓龙6000平台的那些事

    今年3月30日 AMD面向全球发布了代号 Magny Cours 的AMD皓龙6100处理器 在4月19日 AMD在上海召开AMD皓龙6000系列平台发布会 标志着AMD皓龙6000平台正式登录中国 在x86服务器处理器中 AMD皓龙堪称一
  • java中的静态方法是什么

    静态方法是使用公共内存空间的 就是说所有对象都可以直接引用 不需要创建对象再使用该方法 然后在含有main方法的类中使用这个类时 对与以上非静态和静态方法的引用方式是不同的 如下 public class mainClass int sum
  • vscode可以调试c但是无法调试c++程序的

    安装Vscode后调试c程序正常 但是调试c 程序出现问题 出现launch program xxxxxx does not exist的问题 我发现是无法正常生成exe文件导致的问题 当我们对c程序进行调试时 可以正常生成exe文件 但是
  • 太牛逼了!从Python入门到入魔

    总被读者问到 我看完了python入门的书 后面就不知道要学什么了 今天就给你们整理全套入门到进阶的教程 这套教程非常全面而且详细 从Python入门到Python进阶 Django Flask等Web框架以及爬虫 数据库 算法与数据结构等
  • 计算机ip保留地址,分类ip地址中,保留地址有哪些?具体点说说,作业。

    分类ip地址中 保留地址有哪些 具体点说说 作业 以下文字资料是由 历史新知网www lishixinzhi com 小编为大家搜集整理后发布的内容 让我们赶快一起来看一下吧 分类ip地址中 保留地址有哪些 具体点说说 作业 A类地址中的私
  • Vue开发组件库

    Vue开发组件库 1 创建项目 npm install g vue cli vue create 项目名 针对于vue脚手架生成的项目需要做出一些调整 将src重命名为examples 防止第三方开发者产生的歧义 添加examples同级目
  • MySQL CRUD (带样例)

    目录 1 Create 创建 1 1 单行数据 全列插入 1 2 多行数据 指定列插入 1 3 插入否则更新 1 4 替换 2 Retrieve 读取 2 1 SELECT 列 全列查询 指定列查询 查询字段为表达式 为查询结果指定别名 结
  • 螺旋式排列数组

    文章目录 前言 解题思路 上代码 总结 前言 螺旋式排列数组在letcode中属于中等难度的题型 但是对于俺这种道行浅的人再次重新拾起正吃灰的C语言的菜鸟来说确实不容易 其实思路不难 按主要是卡在了二维数组空间的分配问题上 最后调试才将代码
  • 【软件测试】selenium3

    自动化测试的概念 自动化测试指软件测试的自动化 在预设状态下运行应用程序或者系统 预设条件包括正常和异常 最 后评估运行结果 将人为驱动的测试行为转化为机器执行的过程 自动化测试就相当于将人工测试手段进行转换 让代码去执行 提高测试效率 保
  • python中的groupby()函数

    1 groupby 函数介绍 groupby 函数扫描整个序列并且查找连续相同值 或者根据指定key函数返回值相同 的元素序列 在每次迭代的时候 它会返回一个值和一个迭代器对象 这个迭代器对象可以生成元素值全部等于上面那个值的组中所有对象
  • 【深度学习】经典的卷积神经网络模型介绍(LeNet、AlexNet、VGGNet、GoogLeNet、ResNet和MobileNet)

    经典的卷积神经网络模型介绍 卷积神经网络简介 一 LeNet 1 INPUT层 输入层 2 C1层 卷积层 3 S2层 池化层 下采样层 4 C3层 卷积层 5 S4层 池化层 下采样层 6 C5层 卷积层 7 F6层 全连接层 二 Ale
  • 灰色预测模型matlab_灰色预测

    你好 我是goldsunC让我们一起进步吧 文章目录 灰色预测引言灰色预测的类型最简单的模型 GM 1 1 GM 1 1 模型实例原理及求解数据处理方法 1 累加生成2 累减生成3 均值生成求解步骤框图求解步骤小误差概率p及方差比检验标准
  • 企业管理靠员工自觉只能是海市蜃楼

    企业管理靠员工自觉只能是海市蜃楼 凭良心做事好不好 好 要不要凭良心做事 要 但得有前提 这个前提就是这家企业人人讲良心 个个讲良心 特别是老板讲良心 如果老板让员工讲良心 自己不讲良心 讲良心的人不是伤心走了 就是有样学样 也不讲良心了
  • C++进阶 智能指针

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