还在因为指针的释放问题而烦恼吗?快来看下C++11智能指针shared_ptr

news2024/11/24 14:10:28

智能指针是这样一种类,即其对象的特征类似于指针。例如,智能指针可以存储new分配的内存地址,也可以被解除引用。

因为智能指针是一个类对象,因此它可以修改和扩充简单指针的行为。例如,智能指针可以建立引用技术,这样多个对象可共享由智能指针跟踪同一个值,当使用该值的对象数为0是,智能指针将删除这个值。

智能指针可以提高内存的使用效率,帮助防止内存泄漏,但并不要求用户熟悉新的编程技术

00 | 前言

void test1(int& i_num)
{
    int* num = new string(i_num);
    // something to do
    i_num = num;
    return;
}


上面这个函数中存在这样一种缺陷,你发现了吗?

每当调用上面这个函数时,都会从堆中分配内存,但是却没有回收内存,这就导致了内存泄漏的问题,知道的同学都知道只要在最后加上delete num语句就没问题了,不知道的同学就不知道加什么了,所以对不熟悉的同学来说,这个“最后加上delete num”的动作就会经常忘记。那有没有那么一种方法,可以让我们不必关注这个delete num的动作,它在该释放的时候自动释放呢?

对于基本类型来说,暂时是没有这种功能的,但是对于C++类而言,它的析构函数就提供了这样的功能,所以通过类来实现智能指针就可以有效的避免“忘记加上delete语句”导致内存泄漏的情况

int* num1 = new int(2);
int* num2 = new int;
num2 = num1;
delete num2;
cout << *num1 << endl;
cout << *num2 << endl;
delete num1;

再来看上面的代码,能发现其中的缺陷吗?

在这里插入图片描述

两个指针指向同一块地址,那么这时候无论是delete哪个对象,都会释放掉这块内存资源,所以第五行开始输出的就已经不再是本来所指向的资源,而导致num1变成了野指针,最后上面的代码实际对同一个资源对象执行了两次释放动作(资源对象被释放后,如果再去释放该资源,就会会导致系统崩溃)。这就导致在必须使用多个指针指向同一个对象资源的情况下,很容易因为内存的错误释放或者操作已经释放的指针而导致系统崩溃,那怎么规避这个问题?

万不可能说拿笔记住每一个指针声明及调用的地方,人工保证每一个指针的正确声明,引用及释放问题。而对类而言,可以通过对这个智能指针类进行扩展功能来保证共享的资源只会被释放一次,从而有效的解决这个问题。而智能指针shared_ptr就应运而生了

01 | 概念

智能指针shared_ptr的本质是一个用于管理动态内存分配的模板类,它的设计意图是为了解决“当多个智能指针指向同一个对象资源时,错误的内存释放导致系统崩溃”问题,它的设计理念是“采用引用计数,使得在多个智能指针指向同一个对象资源时,给每一个指向这个对象资源的智能指针的引用计数进行算数处理,保证这个共享资源的正确释放”

简单来说,shared_ptr就是一个采用引用计数技术(允许多智能指针指向同一共享对象资源时,且保证该资源仅当引用计数为0时才被释放)的一个模板类实现智能指针。

shared_ptr定义在头文件memory中,其模板定义了类似指针的对象,可以将 new 获得(直接或间接)的地址赋给这种对象,同时在类内部定义一份引用计数数据,用来记录这个对象资源被几个对象共享。引用计数规则如下:
1. 当一个shared_pte对象过期时,引用计数减一,并且判断此时引用计数是否为0;
2. 引用计数为0,表示这是最后一个使用该资源的对象,调用其析构函数使用delete来释放内存;
3. 引用计数不为了0,则说明还有其他shared_ptr对象在使用该资源,不可释放资源,否则会导致其他shared_ptr对象称为野指针。

从下面的示例图直观的看一下shared_ptr在多指针指向同一个对象资源时的内存分配情况

shared_ptr<int> p1(new int(1));
shared_ptr<int> p2 = p1;

在这里插入图片描述

图中不难看出,在最后销毁资源的过程中,销毁p2时,引用计数减一后不为0,不对对象资源进行释放,当p1销毁时,引用计数为0,此时才释放对象资源,从而保证了共享资源只被释放一次,防止了内存泄漏问题

02 | 实现

前面提到shared_ptr的本质是一个模板类,既然是类,那么一定会有构造函数以及析构函数,再加上引用计数原则,把这个模板类的基础框架搭建起来,代码如下

template<class T>
class shared_ptr
{
    public:
        shared_ptr(T* i_sPtr):ptr(i_sPtr)
        {
            count = new int(1);
        }
        ~shared_ptr()
        {
            if (--(*count) == 0)
            {
                delete count;
                delete ptr;
                count = nullptr;
                ptr = nullptr;
            }
        }
    private:
        T* ptr;
        int* count;
};

上面的框架只实现了构造函数和析构函数,但是对于以下代码操作

shared_ptr<int> p1(new int(1));
shared_ptr<int> p2(p1);
shared_ptr<int> p3 = p1;
shared_ptr<int> p4(new int(2));
p3 = p4;

还需要增加拷贝构造函数以及赋值重载函数的实现,两个函数实现代码如下:

shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count)
{
    (*count)++;
}

shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
{
    if (i_sPtr.ptr != ptr)
    {
        delete ptr;
        delete count;
        ptr = i_sPtr.ptr;
        count = i_sPtr.count;
        (*count)++;
    }
    return *this;
}

既然是智能指针,那么就一定可以使用运算符"*"和“->”,所以还需要重载这两个操作符,实现代码如下:

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

最后还要加上shared_ptr的常用函数之一use_count() —— 返回当前的引用计数值,至此,智能指针shared_ptr的基本功能实现完成了,整合起来就是

template<class T>
class shared_ptr
{
    public:
        shared_ptr(T* i_sPtr):ptr(i_sPtr)
        {
            count = new int(1);
        }
        ~shared_ptr()
        {
            if (--(*count) == 0)
            {
                delete count;
                delete ptr;
                count = nullptr;
                ptr = nullptr;
            }
        }
        shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count)
        {
            (*count)++;
        }
        shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
        {
            if (i_sPtr.ptr != ptr)
            {
                delete ptr;
                delete count;
                ptr = i_sPtr.ptr;
                count = i_sPtr.count;
                (*count)++;
            }
            return *this;
        }
        T& operator*()
        {
            return *ptr;
        }
        T& operator->()
        {
            return ptr;
        }
        int& use_count()
        {
            return *count;
        }

    private:
        T* ptr;
        int* count;
};

综上所述,C++的模板类使得可以通过构造函数将shared_ptr的类对象初始化为一个拟常规指针(类似于常规指针,但是特性比常规指针要多)
那么以上实现的智能指针 s h a r e d − p t r 就完美了吗? \color{pink}{那么以上实现的智能指针shared-ptr就完美了吗?} 那么以上实现的智能指针sharedptr就完美了吗?

03 | 问题

shared_ptr并非万灵丹

  • 对象类型格式问题 \color{pink}{对象类型格式问题} 对象类型格式问题

      shared_ptr<int> p1(new int[3]);
    

    仔细回想下C++中关于new和delete操作符的描述——对于指针和动态数组,C++中new和delete需要使用相应的方式,指针:new && delete动态数组数组:new[] && delete[]

    而在自己实现的的shared_ptr类代码中,申请内存空间和释放内存资源时使用的是 new 和 delete,而上面这行代码因为对象类型格式不匹配导致的后果在目前来说是不确定的,这就是当前实现的shared_ptr问题之一。

    想要上面这行代码执行正常,有两种方法:

    1. 可以重新定义一个shared_ptr1,将对应的操作符格式修改即可

    2. 自定义删除器,在构造时传递一个函数指针,用于释放对象的内存;

    下面是一个实现shared_ptr自定义删除器的示例代码:

    #include <iostream>
    #include <memory>
    #include <cstring>
    
    using namespace std;
    void custom_deleter(char* p)
    {
    	cout << "custom deleter called" << endl;
    	delete[] p;
    }
    
    int main()
    {
    	shared_ptr<char> sp(new char[10], &custom_deleter);
    	strcpy(sp.get(), "example");
    	cout << sp.get() << endl;
    	return 0;
    }
    

    在这个例子中,我们定义了一个名为custom_deleter的自定义删除器函数,在函数中输出“custom deleter called”作为演示目的。然后,我们创建一个带有指向10字节字符缓冲区的shared_ptr,并将自定义删除器函数的地址传递给它。在程序的其余部分,我们使用strcpy函数向缓冲区复制字符串“example”,并打印它们。

    当程序退出作用域时,shared_ptr对象将在结束生命周期时调用自定义删除器。在这个例子中,我们的自定义删除器函数将打印一条消息,并释放存储在动态内存中的字符缓冲区。

  • 线程安全问题 \color{pink}{线程安全问题} 线程安全问题

    shared_ptr的引用计数本身是安全且无锁的,但对象的读写不是,它有两个数据成员,读写操作不能原子化。

    • 一个 shared_ptr 对象实体可被多个线程同时读取

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

    针对线程安全问题,对上述实现代码进行修改后,如下

      template<class T>
      class shared_ptr
      {
      	public:
          	shared_ptr(T* i_sPtr):ptr(i_sPtr)
          	{
              	count = new int(1);
              	mtx = new mutex;
          	}
          	void deletePtr()
          	{
              	bool flg = false;
              	mtx->lock();
              	if (--(*count) == 0)
              	{
                  	flg = true;
                  	delete count;
                  	delete ptr;
                  	count = nullptr;
                  	ptr = nullptr;
              	}
              	mtx->unlock();
              	if (flg)
              	{
                  	delete mtx;
                  	mtx = nullptr;
              	}
          	}
          	~shared_ptr()
          	{
              	deletePtr();
          	}
          	shared_ptr(shared_ptr<T>& i_sPtr):sPtr(i_sPtr.ptr), count(i_sPtr.count), mtx(i_sPtr.mtx)
          	{
              	mtx->lock();
              	(*count)++;
              	mtx->unlock();
          	}
          	shared_ptr<T>& operator=(const shared_ptr<T>& i_sPtr)
          	{
              	if (i_sPtr.ptr != ptr)
              	{
                  	deletePtr();
                  	ptr = i_sPtr.ptr;
                  	count = i_sPtr.ptr;
                  	mtx = i_sPtr.mtx;
                  	mtx->lock();
                  	(*count)++;
                  	mtx->unlock();
              	}
              	return *this;
          	}
          	T& operator*()
          	{
              	return *ptr;
          	}
          	T& operator->()
          	{
              	return ptr;
          	}
          	int& use_count()
          	{
              	return *count;
          	}
    
      	private:
          	T* ptr;
          	int* count;
          	mutex* mtx;
      };
    
  • 循环引用问题 \color{pink}{循环引用问题} 循环引用问题

    在使用shared_ptr时,当两个或多个对象互相持有对方的引用,导致它们的引用计数永远不会降为零,从而导致内存泄漏的情况。

    看下面这一个例子

      #include <iostream>
      #include <memory>
    
      using namespace std;
      class A;
      class B;
    
      class A
      {
      	public:
          	A(){cout << "A done" << endl;}
          	~A(){cout << "A kill" << endl;}
          	void test(shared_ptr<B> i_ptrb){ptr = i_ptrb;}
      	private:
          	shared_ptr<B> ptr;
      };
    
      class B
      {
      	public:
          	B(){cout << "B done" << endl;}
          	~B(){cout << "B kill" << endl;}
          	void test(shared_ptr<A> i_ptra){ptr = i_ptra;}
      	private:
          	shared_ptr<A> ptr;
      };
    
      int main()
      {
      	shared_ptr<A> a(new A);
      	shared_ptr<B> b(new B);
      	cout << "countA = " << a.use_count() << ", countB = " << b.use_count() << endl;
      	a->test(b);
      	b->test(a);
      	cout << "countA = " << a.use_count() << ", countB = " << b.use_count() << endl;
      	return 0;
      }
    

    在这里插入图片描述

    从输出的结果上可以看出异常 —— 两个类对象的构造函数都正常执行了,但是在程序退出的时候,并没有执行对应的析构函数,为什么?可以看到输出结果中程序推出前,两个对象的引用计数不是0,而是2,那么程序退出时引用计数一定不会等于0,就一定不会释放资源,从而导致内存泄漏问题,通过图形化演示一遍上述代码的关键执行过程,如下

    在这里插入图片描述

    1. 对象初始化时,两个对象实例的引用计数都至为1;

    2. 调用test()函数时,都分别引用了对方,所以引用计数都加一为2;

    3. 程序结束后,分别调用了边自身的析构函数,引用计数减一为1;

    此时引用计数为1,不为0,析构函数并不会去释放内存资源。

    那么怎么解决循环引用的问题呢?

    C++库中存在weak_ptr类型智能指针,weak_ptr对象可以指向shared_ptr对象,且不会增加shared_ptr对象中的引用计数

    通过使用标准库中的weak_ptr智能指针可以很好的解决循环引用问题,方法如下

    将其中一个类的对象成员修改为weak_ptr类型智能指针,那么为什么不是两个都用呢?

    通过代码调试来看下为什么只修改其中一个为weak_ptr类型智能指针就能解决循环引用问题

    class A
    {
    	public:
        	A(){cout << "A done" << endl;}
        	~A(){cout << "A kill" << endl;}
        	void test(shared_ptr<B> i_ptrb){ptr = i_ptrb;}
    	private:
        	shared_ptr<B> ptr; //weak_ptr<B> ptr
    };
    
    class B
    {
    	public:
        	B(){cout << "B done" << endl;}
        	~B(){cout << "B kill" << endl;}
        	void test(shared_ptr<A> i_ptra){ptr = i_ptra;}
    	private:
        	weak_ptr<A> ptr;
    };
    

    在这里插入A描述

    仅修改类B结果图

    在这里插入图片描述

    修改类B和类A结果图

    从上面两个执行结果可以看出,无论修改一个类还是修改两个类,都能正常释放内存资源了,所以只需要修改一个类即可,那么为什么修改一个类对象成员就可以了呢?

    结果上可以看到执行完a→test(b)的时候,a的引用计数并没有改变,所以在程序执行结束的时候,a正常执行了析构函数,并释放了对应的内存资源,也就不会再引用b对象的资源,所以a正常销毁之后,b的计数也正常减一变为1,此时再执行自身的析构函数就可以正常释放内存资源了

04 | 总结

当需要多个指针共享同一个对象并且确保对象的内存被正确释放时,shared_ptr是一种方便可靠的内存管理工具。可以通过shared_ptr创建一个指向动态内存对象的智能指针,并且它支持自定义删除器,自动引用计数以及弱指针。

shared_ptr具有以下特点:

  • 可以安全地指向动态分配的对象,避免内存泄漏和悬挂指针的问题。

  • 引用计数机制确保了多个指向同一个对象的shared_ptr能够准确地共享所指向对象的访问权。

  • 智能指针本身在销毁时自动释放内存,不需要程序员显式调用delete函数。

  • 支持自定义删除器,可以在需要特殊操作(如文件关闭、数据库连接断开等)时回收动态分配的对象。

总之,shared_ptr在C++中是一种强大且灵活的工具,可以使代码更加简洁、安全和易于维护。但需要注意的是过度使用shared_ptr可能会导致引用计数的开销变得非常大,从而影响程序的运行效率。

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

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

相关文章

读研读博不emo

文章目录 起因积极走出封闭的孤独经历一——去心理健康中心看看书 原因剖析解决思路 好久没更b站&#xff0c; 起因 起因是这样的&#xff0c;在自己读研的研一下学期。也遭遇到了年以来的第一次心态发生有点压抑的情况吧。其实第一学期因为我遇到了很好的导师&#xff0c;然后…

二叉搜索树(Binary Search Tree)的深入剖析:代码+画图 详解插入、查找、删除操作

BSTree 0 引言1 二叉搜索树的概念2 创建一棵二叉搜索树&#xff08;插入操作&#xff09;2.1 画图分析插入操作2.2 代码思路2.3 利用中序遍历验证 3 二叉搜索树的查找操作4 二叉树搜索树的删除操作&#xff08;重点&#xff09;4.1 代码的一些细节分析 5 总结 0 引言 本篇文章…

【Elasticsearch】集群部署

文章目录 本地集群Windiows创建 elasticsearch-cluster 文件夹&#xff0c;在内部复制三个 elasticsearch 服务修改集群文件目录中每个节点的 config/elasticsearch.yml 配置文件启动集群测试集群-查看集群状态 本地开启集群Linux软件下载软件安装创建用户修改配置文件启动软件…

链表及链表的常见操作和用js封装一个链表

最近在学数据结构和算法&#xff0c;正好将学习的东西记录下来&#xff0c;我是跟着一个b站博主学习的&#xff0c;是使用js来进行讲解的&#xff0c;待会也会在文章后面附上视频链接地址&#xff0c;大家想学习的可以去看看 本文主要讲解单向链表&#xff0c;双向链表后续也会…

Java后端:html转pdf实战笔记

目录 1、htmltopdf有什么用&#xff1f; 2、什么是wkhtmltopdf 3、wkhtmltopdf 参数介绍 4、示例项目 5、预览效果 1、htmltopdf有什么用&#xff1f; htmltopdf 是一款基于wkhtmltopdf技术的html转pdf文档java类库&#xff0c;支持html转pdf和url转pdf。 2、什么是wkhtmltopdf…

Renesa 瑞萨 A4M2 移植文件系统FAT32

配置SDIO底层驱动&#xff08;SD card&#xff09; 跳到对应GPIO&#xff0c;进行复用 将Operation Mode配置 SD_MMC 4Bit&#xff0c;系统会自动配置 会到stacks 根据上面提示&#xff0c;解决错误。 解决第一个error mmc配置 mmc 以上SD卡底层配置完成 使用串口作为…

linux rs485功能增加

目录 串口驱动层级结构 485配置流程 dts相关 配置注册 初始化 485收发切换 delay_after_send 目前linux 内核中已经支持了485的实现&#xff0c;但由于底层驱动的支持情况&#xff0c;导致我们采用不同芯片时需要对底层驱动进行修改&#xff0c;以满足内核485的各个回调…

Linux-基本指令2

文章目录 touch&#xff08;新建一个文件&#xff09;whoami(查看当前用户名)概念&#xff1a;1.你是如何看待指令的&#xff1f;2.我们在执行指令之前&#xff0c;我们应该先做什么? 概念&#xff1a;/tree . (树状显示文件夹和文件)rmdir && rmrmdirrm ctrl c通配符…

【MFAC】基于全格式动态线性化的无模型自适应控制(Matlab代码)

例题来源&#xff1a;侯忠生教授的《无模型自适应控制&#xff1a;理论与应用》&#xff08;2013年科学出版社&#xff09;。 &#x1f449;对应书本 4.4 单输入单输出系统(SISO)全格式动态线性化(FFDL)的无模型自适应控制(MFAC) 上两篇博客分别介绍了基于紧格式和偏格式动态线…

软件分享--安卓纯文本记事本软件,支持多记事本与密码

文章目录 软件名字&#xff1a;LS记事本支持多记事本安全性&#xff1a;备份和恢复&#xff1a;搜索功能&#xff1a;显示功能&#xff1a;字体调节&#xff1a;轻量绿色下载地址 软件名字&#xff1a;LS记事本 支持多记事本 安全性&#xff1a; 1.每个记事本支持设置访问密码…

【Elasticsearch】几点核心概念

文章目录 核心概念系统架构分布式集群单节点集群故障转移水平扩容应对故障 路由计算(确定哪个主分片)分片控制&#xff08;确定哪个节点&#xff09;创建个集群如何查看数据呢&#xff1f;写流程读流程更新流程 分片原理倒序索引文档搜索动态更新索引持久化变更 文档分析内置分…

“Lunar Lobster “现已经可以下载

Canonical近日发布了Ubuntu 23.04&#xff08;Lunar Lobster&#xff09;操作系统&#xff0c;这是对其流行的GNU/Linux发行版的一次重大更新&#xff0c;带来了一些最新和最伟大的技术和开源软件。 被称为 “Lunar Lobster”&#xff0c;Ubuntu 23.04由最新的Linux 6.2内核系列…

【计算机网络】学习笔记:第五章 传输层【王道考研】

基于本人观看学习b站王道计算机网络课程所做的笔记&#xff0c;不做任何获利 仅进行交流分享 特此鸣谢王道考研 若有侵权请联系&#xff0c;立删 如果本篇笔记帮助到了你&#xff0c;还请点赞 关注 支持一下 ♡>&#x16966;<)!! 主页专栏有更多&#xff0c;如有疑问欢迎…

【数据结构与算法】哈希—— 位图 | 布隆过滤器 | 哈希切割

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《数据结构与算法》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 哈希是一种映射思想&#xff0c;这里再讲解两种应用哈希思想的数据结构。 哈希—— 位图 | …

边学边记——数据结构☞堆(包括优先级队列的介绍)

目录 一、堆&#xff08;二叉堆&#xff09; 堆是用来干什么的&#xff1f; 堆是用什么存储结构存储的&#xff1f; 1. 堆的概念 2. 堆的性质 3. 堆的存储方式 下标之间的规则&#xff1a; 4. 堆的基本操作 (1). 向下调整/堆化操作 (2).堆的创建 (3).堆的插入 (4…

2023-04-30:用go语言重写ffmpeg的resampling_audio.c示例,它实现了音频重采样的功能。

2023-04-30&#xff1a;用go语言重写ffmpeg的resampling_audio.c示例&#xff0c;它实现了音频重采样的功能。 答案2023-04-30&#xff1a; resampling_audio.c 是 FFmpeg 中的一个源文件&#xff0c;其主要功能是实现音频重采样。 音频重采样是指将一段音频数据从一个采样率…

duubo+zookeeper

1、Dubbo简介 1. Dubbo是什么&#xff1f; 高性能、轻量级、开源、基于java Dubbo 是阿里集团开源的远程服务调用的分布式框架&#xff08;告别Web Service模式中的WSDL&#xff0c;以服务者与消费者的方式在dubbo上注册&#xff09; 协议和序列化框架都可以插拔是及其鲜明…

【办公类-22-04】周计划系列(4)-生成“周计划”(提取旧docx指定段落的内容,写入EXCLE模板,再次生成新docx)

代码相当复杂&#xff0c;操作很繁琐&#xff0c;自己都要研究半天T_T 文件夹展示 01提取提取新表的已有内容&#xff08;提取大8班、大7班的新版本里面的额内容&#xff09; &#xff08;需要里面的一些反思&#xff0c;用来占位&#xff09; 这里有一份根据新模板用Python批…

spring2:创建和使用

目录 1.创建Spring项目 1.1创建Maven类 1.2添加Spring支持框架 1.3添加启动类 2.存储Bean对象 2.0 spring项目中添加配置文件(第一次) 2.1创建Bean 2.2把Bean注册到容器中 3.获取并使用Bean对象 3.1创建上下文 3.2获取指定Bean对象 getBean()方法 --> 获取什么…

双周赛103(模拟、网格图BFS、树状数组)

文章目录 双周赛103[6406. K 个元素的最大和](https://leetcode.cn/problems/maximum-sum-with-exactly-k-elements/)模拟 [6405. 找到两个数组的前缀公共数组](https://leetcode.cn/problems/find-the-prefix-common-array-of-two-arrays/)模拟 [6403. 网格图中鱼的最大数目](…