• C++11智能指针之02


    一、什么是智能指针

    背景

    我们在C++中通过new/delete进行内存分配和释放,但是实际编码过程中可能由于程序员的疏忽导致内存泄漏问题的出现。 在Java中GC(垃圾回收机制)进行内存管理。C++11同样增加了内存管理功能,这就是智能指针。

    • 智能指针本质上是一个模板类,一般使用的这个类的对象,而不是指针
    • 智能指针体现在内存释放问题上,用智能指针管理new的对象,将不在需要手动delete

    智能指针需要包含#include 头文件, 在C++11之后才支持

    实现原理

    智能指针是存储指向动态分配(堆区)对象指针的类,内部通过引用计数实现对管理对象内存的监控,每使用一次引用计数+1,每析构一次引用计数减1,当减为0,释放所指向管理对象的内存。

    通过RAII机制,用于生存期的控制,当离开智能智能的作用域会自动销毁动态分配的对象,防止发生内存泄漏问题。

    分类

    C++11提供了三种智能指针

    1. std::shared_ptr 共享的智能指针
      get()函数:返回数据的指针的引用
      use_count() : 返回管理对象的智能指针个数
      swap() : 交换两个智能指针管理的对象
      reset() : 重置智能指针对象
      针对部分场景需要自己手动添加删除器去释放对象内存

    2. std::unique_ptr 独占的智能指针
      禁止拷贝和赋值 独占型
      任何时候unique_ptr操作管理对象,永远只有一个有效
      可以通过move()函数转交所有权
      reset函数结合release函数移交所有权

    3. std::weak_ptr 弱引用的智能指针,它不共享指针,不能操作资源,是用来shared_ptr的
      弱引用指针,不会累计计数
      weak_ptr只能通过shared_ptr或者weak_ptr来构造
      主要应用场景:为了解决shared_ptr循环引用内存导致无法释放问题
      不能使用*访问,可以使用->取值
      通过成员函数lock()获取shared_ptr对象然后在访问数据

    二、shared_ptr

    shared_ptr 共享的智能指针 : 可以多个共享智能指针同时管理同一个对象内存

    初始化

    有三种方式初始化

    • ​ 构造函数方式

      ​ shared_ptr sp1(new int(5));

    • std::make_shared辅助函数

      shared_ptr sp1 = std::make_shared(5);

    • reset()方法

      ​ sp1.reset(new int(5));

      结论:推荐使用make_shared辅助函数进行初始化

    方法

    use_count引用计数
    get()获取智能指针管理对象的原始指针,但是不建议这么用,因为获取原始指针可能有错误导致内存被delete
    operator*返回智能指针所管理对象的解引用,相当于指针的解引用*p
    operator->智能指针所指向对象的指针
    reset()重置当前存储的指针
    • use_count : 可以查看当前有多少个智能指针同时管理着这同一块内存

      ​ (unique_ptr无该方法,它是独占型智能指针)

    • get() : 获取管理内存的原始地址

      注意:不建议使用get()函数获取 shared_ptr 关联的原始指针,因为获取原始指针可能会有错误操作导致原始指针的delete操作,导致智能指针使用异常。

      对于基本数据类型,通过智能指针和普通指针管理内存效果是一样的

      ​ 例如: shared_ptr sp1(new int(8)); (shared_ptr重载了*解引用和->运算符)

      ​ cout<<“sp1”<<*sp1<

      ​ 也可 int *p1 = sp1.get(); //使用get()方法效果一样

      ​ 对于对象通过get()方法可获取原始地址

      ​ shared_ptr sp1(new Person(8));

      ​ Person *p = sp1.get();

    • reset() : 重置当前存储的指针

      ​ 如果当前智能指针引用计数大于1则把该智能指针置位为nullptr且引用计数-1,如果当前智能指针引用计数等于1,则释放指向对象的内存并把该指针指针置位为nullptr.

    • operator* : 返回智能指针所指向对象的解引用 相当于int *p = new int(8); *p

    • operator->: 返回智能指针所指向对象的指针,以便于访问成员,跟get()函数一样的效果。 相当于指针->

    • 指定删除器

      当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

    1. shared_ptr管理单个对象
        1.简单举例
        shared_ptr ppp(new Test(100), [](Test* t) {
            //释放内存
            cout << "Test对象的内存被释放了......." << endl;
            delete t;
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.shared_ptr管理数组时需要对数组指定删除器

    默认情况下,shared_ptr指向的动态的内存是使用delete来删除的。这和我们手动去调用delete然后调用对象内部的析构函数是一样的。与unique_ptr不同,shared_ptr不直接管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器来替代delete 。

     shared_ptr ppp(new int[10]), [](int* t) {
            //释放内存
            cout << "Test对象的内存被释放了......." << endl;
            delete[] t;  //这里是数组
        });
    
    • 1
    • 2
    • 3
    • 4
    • 5

    shared_ptr使用禁忌

    注意以下都是错误示例代码,应该避免出现下面用法

    1. 不能使用原始指针初始化多个shared_ptr。

      int* p1 = new int(10);
      std::shared_ptr p2(p1);
      std::shared_ptr p3(p1);
      // 由于p2和p3是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针,
      //比如p1已经delete了,这时候p2和p3再操作就是空悬指针了
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    2. 不允许以暴露裸漏的指针进行赋值

    //带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样
    std::shared_ptr p1 = new int();//不能隐式转换,类型不匹配
    
    • 1
    • 2

    3.不要用栈中的指针构造 shared_ptr 对象

    int x = 12;
    std::shared_ptr ptr(&x);
    //shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。
    
    • 1
    • 2
    • 3

    4.不要使用shared_ptr的get()初始化另一个shared_ptr

    Base *a = new Base();
    std::shared_ptr p1(a);
    std::shared_ptr p2(p1.get());
    //p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题
    
    • 1
    • 2
    • 3
    • 4

    5.多线程中使用 shared_ptr需要加锁保护

    shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

    一个 shared_ptr 对象实体可被多个线程同时读取
    两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
    如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁

    三、unique_ptr

    独占型智能指针:同一时刻只能有一个指针管理对象内存。

    去掉了use_count() 方法

    删除了构造函数和=运算符重载

    可通过move函数完成所有权的转移

    reset()将智能指针置为nullptr

        //初始化通过构造函数和make_unique
    	unique_ptr up1(new int(20));
    	unique_ptr up2 = std::make_unique(30);
    
    	//delete =运算符 因为是独占性指针
    	//unique_ptr up3 = up1;   //error    delete = 
    
    	//通过move转移所有权
    	unique_ptr up3 = std::move(up1);
    	if (!up1) {
    		cout << "up1 is nullptr" << endl;
    	}
    	int* p = up3.get();
    	cout << "up3=" << *up3 << endl;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    四、weak_ptr

    原理

    弱引用std::weak_ptr可以看成是shared_ptr的助手,它不管理shared_ptr内部的指针。weak_ptr没有重载*和->运算符,因此它不共享指针也不能操作资源。它的构造和析构函数也不会导致引用计数的变化。主要作用是作为一个旁观者监视shared_ptr中管理的资源是否存在。

    常用方法

    • std::weak_ptr类提供的expired()方法来判断观测的资源是否已经被释放

      if(wp1.expired()){

      ​ cout<<“weak ptr is expired!!!”<

      } else {

      ​ // To do something

      }

    • std::weak_ptr类提供的lock()方法来获取管理所监测资源的shared_ptr对象

      ​ shared_ptr sp1 = wp1.lock();

    • std::weak_ptr类提供的reset()方法来清空对象,使其不监测任何资源

    应用场景

    1. 返回管理this的shared_ptr

    2. 解决循环引用问题

    代码demo

    #include 
    #include 
    
    using namespace std;
    
    int main()
    {
    	shared_ptr sp1 = std::make_shared(10);
    	if (!sp1) {
    		cout << "sp1 is nullptr" << endl;
    	}
    
    	//weak_ptr初始化通过构造函数: 传入shared_ptr
    	weak_ptr wp1(sp1);
    	cout << "wp1.use_count():" << wp1.use_count() << endl;
    	//判断weak_ptr监视的资源是否已经被释放
    	if (wp1.expired())
    	{
    		cout << "weakptr manage memory is expired!!!";
    	} else {
    		//通过lock()方法取管理所监测资源的shared_ptr对象
    		shared_ptr spTest1 = wp1.lock();
    		cout << "spTest1=" << *spTest1 << endl;
    	}
    
    	//通过reset()方法清空对象,使其不检测任何对象
    	wp1.reset();
    	if (wp1.expired())
    	{
    		cout << "after reset weakptr manage memory is expired!!!";
    	}
    	weak_ptr wp2;
    	//通过=辅助shared_ptr给weak_ptr
    	wp2 = sp1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    五 应用场景辨析

    1、我们知道shared_ptr可以有如下两种方式初始化,那如何选择呢?

    • 构造函数方式

      此时也有两种方式

      1. shared_ptr sp1(new int(7));

      2. int *p1 = new int(7);

        shared_ptr sp1(p1);

        那么第二种方式有如何问题?直接上代码

            int* p1 = new int(7);
            shared_ptr ptr1(p1);
        
            //通过new int(7)初始化的shared_ptr,如果由于程序员误操作delete内存之后
            {
                delete p1;
                p1 = nullptr;
            }
        
            //此时在使用shared_ptr去操作内存就有问题
            cout << "ptr1.use_count()=" << ptr1.use_count() << endl;
            if (ptr1) {
                cout << "*ptr1=" << *ptr1 << endl;
            } else {
                cout << "ptr1 is nullptr" << endl;
            }
            
            分析:当用p1 delete内存之后shared_ptr还指向原来的内存地址,但此时内存已被释放,在操作就会出问题。 如下图:
            int* p1 = new int(7);
            shared_ptr ptr1(p1);
        
            //通过new int(7)初始化的shared_ptr,如果由于程序员误操作delete内存之后
            {
                delete p1;
                p1 = nullptr;
            }
        
            //此时在使用shared_ptr去操作内存就有问题
            cout << "ptr1.use_count()=" << ptr1.use_count() << endl;
            if (ptr1) {
                cout << "*ptr1=" << *ptr1 << endl;
            } else {
                cout << "ptr1 is nullptr" << endl;
            }
            
            
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 18
        • 19
        • 20
        • 21
        • 22
        • 23
        • 24
        • 25
        • 26
        • 27
        • 28
        • 29
        • 30
        • 31
        • 32
        • 33
        • 34
        • 35
        • 36

        分析:当用p1 误删shared_ptr管理的内存时在操作就会出问题

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VHvPArfU-1668493613867)(C:\Users\100011704\Desktop\555.png)]

    • make_shared方式

      shared_ptr sp1 = std::make_shared(7);

      推荐使用该方式进行初始化

      结论:推荐使用make_shared方式进行初始化, 避免在使用智能指针的时候还用到new关键字去分配内存。

      ​ 使用智能指针就是方便内存管理,防止出现内存泄漏问题。

  • 相关阅读:
    线上研讨会 | CATIA助力AI提升汽车造型设计
    openssl官网文档资料
    vue项目中定位组件来源的查找思路
    “第四十九天” 机组
    算法查找——分块查找
    【活动系列】那些年写的比较愚蠢的代码
    基于深度学习的物体材质预测
    java计算机毕业设计基于安卓Android的宿舍服务平台APP
    Laya:【音效BUG】在游戏失焦后再次返回游戏导致音效播放异常的问题与解决方案
    21.(arcgis api for js篇)arcgis api for js矩形采集(SketchViewModel)
  • 原文地址:https://blog.csdn.net/weixin_45312249/article/details/127675535