C++智能指针使用及原理

news2024/11/25 20:17:16

在讲解之前,先讲述一种RAII思想.

目录

RAII

智能指针原理

auto_ptr

auto_ptr的介绍

auto_ptr的实现

unique_ptr

unique_ptr的介绍

unique_ptr实现

shared_ptr

shared_ptr的介绍

shared_ptr实现

weak_ptr

weak_ptr的介绍

weak_ptr的实现


RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命周期内始终保持有效

template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr)
        :_ptr(ptr)
    {}
    ~SmartPtr()
    {
        delete _ptr;
    }
private:
    T* _ptr;
};

这样我们最后便不再需要显式地释放资源了.

我们创建对象时,可以直接用匿名对象创建,也可以利用已有的指针创建.

int main()
{
    int* ptr1 = new int;
    SmartPtr<int> sp1(ptr1);

    SmartPtr<int> sp2(new int);
}

这样即使在过程中抛异常,对象资源也会正常释放.

智能指针原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因为SmartPtr类中还得需要将* ->重载下,才可让其像指针一样去使用.

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

这样才算是智能指针.

总结满足是智能指针必须有两个条件(原理)

1.RAII特性

2. 重载operator*和opertaor->,具有像指针一样的行为。

是不是满足这两个条件就万事大吉了呢,也不能算是,下面将讲解四种常见的智能指针。

auto_ptr

auto_ptr的介绍

auto_ptr在C++98中就已经存在了,库中也有对应的函数.

class A
{
public:
    A()
    {}
    ~A() 
    {
        cout << "~A()" << endl;
    }
private:
    int _a1 = 0;
    int _a2 = 0;
};
int main()
{
    auto_ptr<A> ap1(new A);
    return 0;
}

这段代码没有任何问题,包括运行起来也能正常释放资源.

但是智能指针存在一个严重的问题:拷贝问题. 

我们如果用我们刚才写的SmartPtr类进行拷贝:

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

这样会引发一个我们之前经常说的问题:同一块资源被析构两次.

 sp2先析构一次,sp1再次析构这时就会出错.说到底还是因为浅拷贝问题.

那么我们改成深拷贝不就可以了吗?

答案也是不能,因为深拷贝就违背了我们的功能需求.

智能指针也是模拟原生指针的行为,我们本身的目的就是把资源交给对象管理。

其实我们仔细想一下,我们把sp1托给sp2,不也就是相当于让sp1和sp2共同管理这份资源吗.

所以我们这里要的就是浅拷贝!

这里和迭代器类似,迭代器浅拷贝没问题的原因是:不管迭代器中资源的释放.

那么库里的auto_ptr是如何解决的呢?

先来说结论:是转移了资源管理权.

一开始在ap1还没有赋值给ap2时,资源还在ap1中.

 当执行完第二条语句:

 我们发现ap1被悬空了,资源管理交给了ap2.

这是一种极不负责的行为,当后面有人想在解引用ap1中的内容时,便会报错.

这算是设计的一种很大的缺陷吧,很多公司也禁止使用auto_ptr.

auto_ptr的实现

namespace hyx {
    template<class T>
    class auto_ptr {
    public:
        auto_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~auto_ptr()
        {
            cout << "~auto_ptr()" << endl;
            delete _ptr;
        }
        auto_ptr(auto_ptr<T>& ap)
            :_ptr(ap._ptr)
        {
            cout << "auto_ptr Delete()" << endl;
            //将原来的指针置空
            ap._ptr = nullptr;
        }
        //以ap1 = ap2为例
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if (this != &ap)
            {
                //如果当前ap1存在,则释放掉ap1
                if (_ptr)
                {
                    cout << "operator= Delete()" << endl;
                    delete _ptr;
                }
                //将ap2的资源转移给ap1
                _ptr = ap._ptr;
                //将ap2的指向置为空
                ap._ptr = nullptr;
            }
            return *this;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };
}

unique_ptr

unique_ptr的介绍

unique_ptr对于拷贝问题设计思路非常简单粗暴:禁止拷贝和赋值.只适用于不需要拷贝的场景.

例如下面这段代码:

    unique_ptr<A> up1(new A);
    unique_ptr<A> up2(up1);//错误,unique_ptr不允许拷贝
    unique_ptr<A> up3 = up2;//错误,unique_ptr不允许赋值

除了拷贝,其它方面和别的智能指针作用几乎是一样的.正常用即可.

unique_ptr实现

不能拷贝和赋值,我们直接将这两个函数设为delete即可,这是C++11中的特性.

而在C++98中,我们只能把它设为私有(防止有人可以在外部实现),并且只声明不实现(防止编译器自己生成).

namespace hyx {
    template<class T>
    class unique_ptr {
    public:
        unique_ptr(T* ptr)
            :_ptr(ptr)
        {}
        ~unique_ptr()
        {
            cout << " unique_ptr" << endl;
            if (_ptr)
                delete _ptr;
        }
        //C++11 将两个函数设为delete
        unique_ptr (unique_ptr <T>& ap) = delete;
        unique_ptr <T>& operator=(unique_ptr <T>& ap) = delete;
        
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
        //C++98防拷贝的方式:私有+只声明不实现
        //private:
        //    unique_ptr(unique_ptr <T>& ap);
        //    unique_ptr <T>& operator=(unique_ptr <T>& ap);
    private:
        T* _ptr;
    };
}

shared_ptr

shared_ptr的介绍

那如果我们就是想要拷贝呢?这个时候shared_ptr便登场了.

C++11中开始提供更靠谱的并且支持拷贝的shared_ptr.

那它是怎么支持的呢?浅拷贝还是深拷贝还是别的方法呢?

看下面这段代码

class A
{
public:
    A()
    {}
    ~A()
    { }

    int _a1 = 0;
    int _a2 = 0;
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2(sp1);
    sp1->_a1++;
    sp1->_a2++;
    cout << sp2->_a1 << "   " << sp2->_a2 << endl;
}

我们把sp1拷贝给了sp2,然后让sp1的两个成员分别+1,然后观察sp2的情况:

发现sp2两个成员也都+1,这就说明它们是共享这块资源的

这不就是浅拷贝吗,那为什么不会出现析构多次出错的情况?

shared_ptr内部其实是采用引用计数的方式来实现的.

具体是怎么做的呢?

有一个计数器count,每当有新的对象来管理时,计数器便++,而每当有对象释放时,这个计数器便--,直到最后一个对象析构时,即count=1时,此时再把这块空间资源释放掉。如此便很好解决了问题.

比如刚才的sp1和sp2共同管理那一块空间资源,此时count=2,当sp2释放时,count--,此时count=1,说明还有其它对象也在管理这块空间,不要清理释放这块资源,所以直接count--即可.

sp1释放时,count=0,说明没有对象再管理这块空间了,此时便清理释放掉这块空间即可.

shared_ptr实现

刚才是我们的想法,但是具体要怎么实现呢?

我们把计数器_count放在哪呢?

如果直接加到类成员里面去,那么每个对象都会有一份独立的_count,这样就无法再进行计数了.

我们想要的_count一定是共享的,那我们把它设置成static可不可以?

答案也是不可以的.看下面的场景就知道了:

    test::shared_ptr<A> sp1(new A);
    test::shared_ptr<A> sp2(sp1);
    test::shared_ptr<A> sp3(sp1);

其中test域是static修饰的_count,主要目的是为了演示.

这样目前看起来没有问题,sp1,sp2,sp3各管理一份,此时_count=3,没任何问题.

但如果此时我又加了一句这样的代码呢?

    test::shared_ptr<int> sp4(new int);

此时我们期望的应该是重新生成一份_count,然后sp4再管理。

但按刚才说的static那种方法,_count会直接++,会让_count=4,这样肯定就错误了.

因为当sp4想释放资源时,由于_count并不为0,会导致sp4的资源不能正常释放.

而按常理来说,sp4和前面的1,2,3管理的并不是同一块资源,sp4不应该和他们公用计数器.此时sp4的_count=0理应被释放的.

 

这就是static修饰所带来的问题.也是为什么不能用static修饰的原因:所有资源都只有一个计数.

我们想要的是每个资源都有一个计数.

这里直接说正确的设计思路:在类内部加了一个指针.int* _pCount;

我们什么时候知道资源 来了呢?构造函数!

来一个新资源我们就new一个,这样就能保证每一份资源都能有计数了.

拷贝构造的时候,不仅把要把_ptr拷贝,也要把_pCount拷贝一份,同时让(*_pCount)++.

析构时,每次先--(*pCount),如果此时_pCount==0说明已经没有对象管理这块资源了,此时再释放掉这块资源.

比较需要注意且麻烦的是 赋值重载运算符.

1.以sp1=sp5为例,由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5.

2.还有防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题

namespace hyx
{
    template<class T>
    class shared_ptr {
    public:
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            ,_pCount(new int(1))
        {}
        ~shared_ptr()
        {
            if (--(*_pCount) == 0)
            {
                cout << "Delete:"  << _ptr  << endl;
                delete _ptr;
                delete _pCount;
            }
        }
        shared_ptr(shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            ,_pCount(sp._pCount)
        {
            (*_pCount)++;
        }
        //以sp1=sp5为例.
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            //防止自己给自己赋值,以及管理相同资源的对象互相赋值,注意不要写this==&sp,因为这样只能解决前者,不能解决后者问题
            if (_ptr == sp._ptr)
            {
                return *this;
            }
            //由于sp1要管理sp5指向的空间资源,此时应该先把sp1的计数--,如果sp1的计数为0,
            //说明sp1是最后一个对象,需要释放掉当前的资源和计数,然后再指向sp5
            if (--(*_pCount) == 0)
            {
                delete _ptr;
                delete _pCount;
            }

            _ptr = sp._ptr;
            _pCount = sp._pCount;

            (*_pCount)++;

            return *this;
        }
        T& operator*()
        {
            return *_ptr;
        }
        T* operator->()
        {
            return _ptr;
        }
    private:
        T* _ptr;
        int* _pCount;
    };
}

我们测试一下下面代码:

int main()
{
    hyx::shared_ptr<A> sp1(new A);
    hyx::shared_ptr<A> sp2(sp1);
    hyx::shared_ptr<A> sp3(sp1);

    hyx::shared_ptr<int> sp4(new int);
    hyx::shared_ptr<A> sp5(new A);
    hyx::shared_ptr<A> sp6(sp5);
}

一共创建了3份资源,所以最后也应该析构3次.

这样完美符合了我们的预期.

weak_ptr

weak_ptr的介绍

shared_ptr看起来很完美了,但是还存在一些小问题:循环引用.

看如下代码

struct ListNode
{
    int _data;
    shared_ptr<ListNode> _prev;
    shared_ptr<ListNode> _next;
    ~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
    shared_ptr<ListNode> node1(new ListNode);
    shared_ptr<ListNode> node2(new ListNode);
    node1->_next = node2;
    node2->_prev = node1;
    return 0;
}

 我们画图来演示问题所在:

 循环引用分析:

1. node1和node2两个智能指针对象分别指向对应的结点,此时引用计数都是1.

2. node1的_next指向node2,node2的引用计数变为2,node2的_prev指向node1,node1的引用计数变成2。

3. node1和node2析构,引用计数分别减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。

4.也就是说node1中_next析构了,node2就释放了;node2中_prev析构了,node1就释放了.

5.但是_next属于node的成员,node1释放了,_next才会析构,而node1由node2中的_prev管理,只有node2释放了,_prev才会释放,而node2又有node1的_next管理,形成了循环,所以这就叫循环引用,谁也不会释放。

为解决循环引用问题,便引用了weak_ptr,但weak_ptr不是常规智能指针,没有RAII,不支持直接管理资源.

 设计出来就是为了解决shared_ptr循环引用问题的.

_prev和_prev是weak_ptr时,它不参与资源释放管理,但是可以访问和修改资源.但是不增加计数,就不存在循环引用的问题了.

// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数
struct ListNode
{
    int _data;
    weak_ptr<ListNode> _prev;
    weak_ptr<ListNode> _next;
    ~ListNode() { cout << "~ListNode()" << endl; }
}; 

weak_ptr的实现

/ 辅助性智能指针,使命就是配合解决shared_ptr循环引用问题
template<class T>
class weak_ptr
{
public:
    weak_ptr()
        :_ptr(nullptr)
    {}
    weak_ptr(const shared_ptr<T>& sp)
        :_ptr(sp.get())
    {}
    weak_ptr(const weak_ptr<T>& wp)
        :_ptr(wp._ptr)
    {}
    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        _ptr = sp.get();
        return *this;
    }
    T& operator*()
    {
        return *_ptr;
    }
    T* operator->()
    {
        return _ptr;
    }
public:
    T* _ptr;
};

这样,智能指针的部分就讲解的差不多了.

其中,shared_ptr是最重要的一个智能指针,如何实现的一定要牢记!

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

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

相关文章

西安大华时代网络科技有限公司

大华时代是行业领先的软件与信息技术服务企业。我们拥有专业的团队和先进的技术&#xff0c;在金融、电信、高科技和互联网等行业&#xff0c;为客户提供优质的产品、先进的解决方案和热情的服务&#xff01;

【SpringBoot + Mybatis系列】插件机制 Interceptor

【SpringBoot Mybatis系列】插件机制 Interceptor 在 Mybatis 中&#xff0c;插件机制提供了非常强大的扩展能力&#xff0c;在 sql 最终执行之前&#xff0c;提供了四个拦截点&#xff0c;支持不同场景的功能扩展 Executor (update, query, flushStatements, commit, rollbac…

mysql check slow_log造成锁-waiting for table level lock

背景&#xff1a; 我们在生产环境使用mysql的时候开启了slow_log 记录表&#xff0c;但有的时候由于记录数据过大&#xff0c;造成该表损坏 ERROR 1194 (HY000): Table slow_log is marked as crashed and should be repaired 这时候就想着用check table 命令来看看表是否正…

云服务器下WordPress发送邮件的设置

WordPress的邮件功能很强大&#xff0c;可以实现用户密码以往后自助恢复等问题。 WordPress默认是使用php发邮件的&#xff0c;php需要配置好smtp&#xff08;端口25&#xff09;服务器及密码。这种方式不直观&#xff0c;因此一般都用smtp插件&#xff0c;常用的插件是WP Mai…

FastDDS 源码剖析:src/cpp/fastdds 源码结构与Publisher源码分析

目录 源码结构 Publisher分析 Publisher 类分析 PublisherIImpl 类分析 源码结构 —builtin:该目录包含FastDDS使用的内置类型和协议的实现。 —core:该目录包含FastDDS库中使用的核心类和函数。这包括处理错误、管理内存和处理线程的类。 --domain:此目录包含DomainPart…

本地资源检测 自定义规则 零基础上手指南

本地资源检测是UWA推出的、面向于静态资源的全量分析。可以全面自动检测项目静态工程内各项资源、代码和设置&#xff0c;能够帮助项目组制定合理的资源与代码标准&#xff0c;及时发现潜在的性能问题和异常错误&#xff0c;建立有效的开发规范。其中“自定义规则”功能特别获得…

docker 安装zookeeper单机版

1. 安装版本3.5.7, 也可以自己去官网找到自己需要的版本复制命令即可 https://hub.docker.com/_/zookeeper/tags docker pull zookeeper:3.5.7 2. 创建映射文件夹&#xff1a; #1. 在centos中创建三个文件夹 mkdir -p /home/zookeeper/conf mkdir -p /home/zookeeper/data mkd…

图片视频抹除算法总结Inpaint

基本是从图片抹水印和视频抹水印两个方向 Video Inpainting&#xff1a;https://paperswithcode.com/task/video-inpaintingImage Inpainting&#xff1a;https://paperswithcode.com/task/image-inpainting 请根据目录查看 图片 Partial Conv 部分卷积层 源自于Image In…

游戏服务器搭建过程中Maven多模块编译遇到的一些问题

目录 1、多模块的创建 1.1 父模块的创建 1.2 删除垃圾文件 1.3 修改pom.xml 1.4 创建子模块继承 2、子模块之间的互相引用 3、多个模块间版本的管理 3.1 dependencis 3.2 dependencyManagement 4、依赖执行 5、在Spring Boot项目中加载依赖项目的组件有几种常用的方法…

十一、PBR材质金属度、粗糙度以及环境贴图的使用

Three.js——十一、PBR材质金属度、粗糙度以及环境贴图的使用 metalness金属度 金属度属性.metalness表示材质像金属的程度, 非金属材料,如木材或石材,使用0.0,金属使用1.0。 new THREE.MeshStandardMaterial({metalness: 1.0,//金属度属性 }) // 或者 // mesh.material.met…

Java面试题-并发篇(2万字带你搞定并发问题)

Java面试题-并发篇 一、线程 1. 线程和进程有什么区别&#xff1f; 线程具有许多传统进程所具有的特征&#xff0c;故又称为轻型进程(Light—Weight Process)或进程元&#xff1b;而把传统的进程称为重型进程(Heavy—Weight Process)&#xff0c;它相当于只有一个线程的任务…

山西电力市场日前价格预测【2023-07-12】

日前价格预测 预测明日&#xff08;2023-07-12&#xff09;山西电力市场全天平均日前电价为446.44元/MWh。其中&#xff0c;最高日前价格为584.92元/MWh&#xff0c;预计出现在12: 00。最低日前电价为325.62元/MWh&#xff0c;预计出现在00: 30。 价差方向预测 1&#xff1a;实…

【操作系统】磁盘调度算法 先来先服务、最短寻道时间优先、扫描算法、循环扫描算法

目录 一、实验要求二、实验原理三、实验内容四、实验结果五、总结 一、实验要求 设计程序模拟先来先服务FCFS、最短寻道时间优先SSTF、扫描算法SCAN和循环扫描算法CSCAN的工作过程。假设有n个磁道号所组成的磁道访问序列&#xff0c;给定开始磁道号m和磁头移动的方向(正向或者反…

【sgRectSelect】Vue实现拖拽鼠标圈选、划区域、框选组件:矩形区域选中checkbox,并回调相关选中、取消选中的操作

边框线虚线动画效果请参阅边框虚线滚动动画特效_虚线滚动效果_你挚爱的强哥的博客-CSDN博客【代码】边框虚线滚动动画特效。_虚线滚动效果https://blog.csdn.net/qq_37860634/article/details/130507289 碰撞检测原理请前往 原生JS完成“一对一、一对多”矩形DIV碰撞检测、碰撞…

嵌入式c语言编码规范

学习嵌入式的同学应该首先掌握嵌入式编码规范&#xff0c;这样才能更好的嵌入式系统。 下面就从这几个方面讲解一下嵌入式c编码规范。 注释风格、排版风格、头文件风格、变量定义、宏定义、函数 1 注释风格 1.1 注释的原则是有助于对程序的阅读和理解&#xff0c;注释不宜太多…

通过(.zip 压缩文件)安装及卸载MySQL

文章目录 一、MySQL安装1.下载MySQL压缩包2.将下载好的压缩包解压到一个没有中文路径的目录下3.配置MySQL环境变量4.验证是否配置成功5.初始化MySQL数据库6.注册MySQL服务7.启动MySQL服务8.修改MySQL数据库默认账户密码9.登录MySQL数据库10.退出MySQL 二、MySQL卸载1.以管理员身…

什么是EDI 180 退货授权和通知?

EDI 180 退货授权和通知是零售商和供应商在退货过程中使用的电子数据交换&#xff08;EDI&#xff09;文件。它既可以作为请求和授权&#xff0c;也可以作为通知文件。 EDI 180 的基本组成部分是什么&#xff1f; EDI 180 交易需要包括有关退货的关键信息。由于EDI 180可以双…

Linux 批量杀掉进程(包含某个关键字)

一、场景说明 现场环境有十多个包含 ”celery” 关键字的进程在运行&#xff0c;每次重启服务&#xff0c;需要将这些进行kill掉&#xff0c;然后重新启动。 可以用如下命令批量kill掉这些进程&#xff1a; kill -9 PID1 PID2 PID3 PID4.....其中&#xff0c;PID是查询到的进…

第九章——内存模型和名称空间

单独编译 C允许程序员将组件函数放在独立的文件中。下面列出了头文件中常包含的内容&#xff1a; 函数原型使用#define或const定义的符号常量结构声明类声明模板声明内联函数 将结构声明放在头文件中是可以的&#xff0c;因为它们不创建变量&#xff0c;而只是在源代码文件…

数据结构--树的存储结构

数据结构–树的存储结构 树的逻辑结构 树是 n ( n ≥ 0 &#xff09; n (n\ge0&#xff09; n(n≥0&#xff09;个结点的有限集合&#xff0c;n 0 时&#xff0c;称为空树&#xff0c;这是一种特殊情况。 在任意一棵非空树中应满足: 1&#xff09;有且仅有一个特定的称为 根 …