C++ 智能指针

news2024/11/29 0:54:34

目录

为什么需要智能指针?

RAII

智能指针的原理

C++智能指针的历史

智能指针

auto_ptr

unique_ptr

shared_ptr

shared_ptr 引用计数解决智能指针拷贝问题

shared_ptr 循环引用问题

智能指针删除器

weak_ptr


为什么需要智能指针?

1. 我们在很多时候,使用new运算符在堆区申请空间,忘记释放则会带来内存泄漏,而智能指针可以自动释放堆区内存,自动执行delete。

2. 异常带来的内存泄漏隐患,可通过智能指针解决。

C++中某些函数或new运算符可能会抛异常,而如果我们在外层函数使用try catch捕获异常,则可能导致程序执行流跳转,即使new的后方有delete语句,也可能会因为捕获异常而不执行,从而造成内存泄漏,此时智能指针就是最好的解决方法。

int div()
{
    int a = 0, b = 0;
    cin >> a >> b;
    if(b == 0)
    {
        throw invalid_argument("除0错误");
    }
    return a / b;
}

void func1()
{
    // p1 p2 div三个地方抛异常会导致不同的内存泄漏
    int* p1 = new int(0);
    int* p2 = new int(1);

    cout << div() << endl;

    delete p1;
    delete p2;
    cout << "p1 and p2 were deleted" << endl;
}

void func2()
{
    unique_ptr<int> up1(new int(1));
    unique_ptr<int> up2(new int(2));

    cout << div() << endl;
}

int main()
{
    try
    {
        func1();
    }
    catch(const invalid_argument& ia)
    {
        cout << ia.what() << endl;
    }
    catch(...)
    {
        cout << "未知错误" << endl;
    }
//    test_auto_ptr();
//    test_smart_ptr2();
//    test_unique_ptr();
//    test_circular_reference();
//    test_weak_ptr();
    return 0;
}

如上,外层函数使用try catch语句,则在new之后发生异常,会导致delete语句被跳过,其中的new 和 div方法都可能抛异常,会造成不同程序的内存泄漏。

使用func2方法中的智能指针即可解决此问题,因为在异常抛出并捕获之后,尽管程序执行流会从div方法或func1(new)中跳转到main,但是函数栈帧仍会从内向外依次销毁,此时作为局部变量的up1 up2生命周期结束,调用析构函数,即可避免内存泄漏。
这就是智能指针使用的RAII思想。

RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

RAII(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。   

借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。


智能指针的原理

1. RAII
有了上面的认识,其实RAII在智能指针这里就是,将堆区资源的创建和释放托管给一个对象,对象生命周期内,内存资源有效,对象生命周期结束时,自动调用析构函数,自动释放内存。

2. 像指针一样
类似于STL容器的迭代器,迭代器通过实现operator* operator->,将迭代器实现出指针的行为,使其使用起来像指针。
而智能指针,本身就是模拟指针,故智能指针类通过实现operator* operator->,使其具有指针一样的操作行为。

3. 智能指针的拷贝行为
我们知道, int* p = new int(1);    int* p2 = p; 此时p和p2指针内存储的地址相同,即指向同样的内存空间。所以,在实现智能指针时,必须考虑智能指针类的拷贝构造 和 operator=的行为。这里不能实现深拷贝,因为原生指针的拷贝本身就是浅拷贝,浅拷贝就是我们的目的。但是若两个智能指针指向相同,一个调用析构之后,另一个智能指针就是野指针。
所以,我们必须考虑如何解决智能指针的拷贝问题。

在C++标准库中,auto_ptr shared_ptr unique_ptr 都有着RAII和像指针一样的特性,但是它们对于指针的拷贝行为解决方法不同。具体看下方...

C++智能指针的历史

C++98中出现了auto_ptr,这是最原始的智能指针,它实现了RAII和像指针一样的行为,所以,可以说是解决了异常带来的内存安全隐患。但是auto_ptr对于智能指针的拷贝的实现非常糟糕...

C++98 ~ C++11之间,boost库实现了scoped_ptr shared_ptr weak_ptr。 在C++11,C++标准库新增了unique_ptr shared_ptr weak_ptr对应boost库的三大智能指针。这三个智能指针都有不同的特性。并且这些智能指针的实现原理是参考boost中的实现的。

智能指针

auto_ptr

    template <class T>
    class auto_ptr
    {
    public:
        explicit auto_ptr(T* p = nullptr)
        :_ptr(p)
        {}
        // auto_ptr拷贝构造的特点:资源转移
        auto_ptr(auto_ptr& ap)
        :_ptr(ap._ptr)
        {
            ap._ptr = nullptr;
        }
        auto_ptr<T>& operator=(auto_ptr<T>& ap)
        {
            if(this != &ap)
            {
                if(_ptr != nullptr)
                {
                    delete _ptr;
                }
                _ptr = ap._ptr;
                ap._ptr = nullptr;
            }
            return *this;
        }

        ~auto_ptr()
        {
            if(_ptr != nullptr)
                delete _ptr;
        }

        // 其实这里的T可能是const int / const Node
        // 明白吗?hhhhhh
        T& operator*() const
        {
           return *_ptr;
        }
        T* operator->() const
        {
            return _ptr;
        }
        T* get() const
        {
            return _ptr;
        }
    private:
        T* _ptr;
    };

auto_ptr的拷贝构造和operator=实现的是资源转移,也就是被拷贝的智能指针将被置为空。 
auto_ptr同样采用了RAII技术以及opeator* operator->

unique_ptr

删除器

    template <class T>
    struct default_delete
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    template <class T>
    struct delete_array
    {
        void operator()(T* p)
        {
            delete[] p;
        }
    };
    template <class T>
    struct Free
    {
        void operator()(T* p)
        {
            free(p);
        }
    };
    template <class T>
    struct Close
    {
        void operator()(T* p)
        {
            close(p);
        }
    };
    // 注意,如果是const int,则删除器就是default_delete<const int>
    template <class T, class D = default_delete<T>>
    class unique_ptr
    {
    public:
        explicit unique_ptr(T* p = nullptr)
        : _ptr(p)
        {}
        ~unique_ptr()
        {
            if(_ptr != nullptr)
            {
                D()(_ptr);
            }
        }
        // unique_ptr不允许拷贝,支持RAII和像指针一样
        unique_ptr(const unique_ptr<T>& up) = delete;
        unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

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

unique_ptr,如名,就是独一无二的智能指针,此智能指针同样采用了RAII和operator* operator->
不同的是对指针拷贝问题的解决:unique_ptr不支持拷贝构造和operator= 

shared_ptr

    template <class T, class D = yzl::default_delete<T>>
    class shared_ptr
    {
    public:
        explicit shared_ptr(T* p)
        :_ptr(p)
        , _pCount(new int(1))
        {}
        shared_ptr(const shared_ptr<T>& sp)
        :_ptr(sp._ptr)
        ,_pCount(sp._pCount)
        {
            // 有线程安全问题,之后学到了再解决。
            ++*_pCount;
        }
        shared_ptr& operator=(const shared_ptr<T>& sp)
        {
            // 防止自赋值
            if(_ptr != sp._ptr)
            {
                Release();
                _ptr = sp._ptr;
                _pCount = sp._pCount;
                ++*_pCount;
            }
            return *this;
        }
        ~shared_ptr()
        {
            Release();
        }

        T& operator*() const
        {
            return *_ptr;
        }
        T* operator->() const
        {
            return _ptr;
        }
        T* get() const
        {
            return _ptr;
        }
        int use_count() const
        {
            return *_pCount;
        }
    private:
        void Release()
        {
            if(--*_pCount == 0)
            {
                if(_ptr != nullptr)
                    D()(_ptr);
                delete _pCount;
            }
        }
    private:
        T* _ptr;
        int* _pCount;
    };

shared_ptr是功能最强大的智能指针,同样实现了RAII技术,operator*,operator->。

shared_ptr 引用计数解决智能指针拷贝问题

同时shared_ptr采用引用计数,实现了智能指针的拷贝。注意,这里的引用计数不能是int类型数据成员(很明显不可以),也不可以是静态int类型数据成员,因为这样的话所有的shared_ptr<int>智能指针使用的都是一个引用计数,不管它们的指向是否相同,这显然也是不行的。
解决方法就是一个int*类型数据成员,在shared_ptr的构造函数中,堆区开辟一个int,初始化为1。在每次拷贝构造 or operator=中++该引用计数。

shared_ptr 循环引用问题

    // 循环引用问题
    struct Node
    {
        int _val;

        std::shared_ptr<Node> _next;
        std::shared_ptr<Node> _prev;

//        std::weak_ptr<Node> _next;
//        std::weak_ptr<Node> _prev;

        ~Node()
        {
            cout << "~Node" << endl;
        }
    };

using yzl::Node;
// 循环引用
void test_circular_reference()
{
    shared_ptr<yzl::Node> n1(new yzl::Node);
    shared_ptr<Node> n2(new Node);

    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;

    // 解决循环引用:将内部的智能指针变为weak_ptr,而不是shared_ptr
    // 调用 weak_ptr<T>& operator=(const shared_ptr<T>& sp);
    n1->_next = n2;
    n2->_prev = n1;

//    n1->_next.lock()->_val = 10;
//    cout << n2->_val << endl;
    cout << n1.use_count() << endl;
    cout << n2.use_count() << endl;
};

 

形如上方场景,比如某类型(Node)中有shared_ptr类型数据成员,此时n1 和 n2本身也是两个智能指针。如上就会造成循环引用的问题:即当函数栈帧销毁,n1 n2 析构,此时引用计数--,因为_next _prev分别指向对方,引用计数减为1,不会释放资源。
此时左边Node想要释放,需要右边Node的_prev调用析构函数(引用计数减为0),而_prev作为Node的自定义类型数据成员,右边Node被析构时会自动调用_prev的析构函数,右边Node析构需要左边_next调用析构,左边_next调用析构需要左边Node释放。
此时的场景就会造成循环引用。

解决方法是,将Node的智能指针数据成员变为weak_ptr类型,weak_ptr的作用其实就是为了解决shared_ptr的循环引用问题。  解决方法是:weak_ptr不会增加引用计数,因此左右两边Node的引用计数分别为1,n1 n2调用析构,资源得以释放。

智能指针删除器

因为new/delete  new[]/delete[]   malloc/free  fopen/fclose(FILE*) 要配对使用,因此智能指针的析构函数不能固定实现为delete。

方式就是使用仿函数(函数对象),仿函数实现了operator()

    template <class T>
    struct default_delete
    {
        void operator()(T* p)
        {
            delete p;
        }
    };
    template <class T>
    struct delete_array
    {
        void operator()(T* p)
        {
            delete[] p;
        }
    };
    template <class T>
    struct Free
    {
        void operator()(T* p)
        {
            free(p);
        }
    };
    template <class T>
    struct Close
    {
        void operator()(T* p)
        {
            close(p);
        }
    };

注意:STL中unique_ptr的删除器是类模板参数,
如 unique_ptr<int, delete_array<int>> up(new int[3]);   即必须在类模板实参处传递仿函数类型
而std::shared_ptr的删除器实现在了构造函数的模板参数中,因此shared_ptr可以通过构造函数时传函数对象/lambda的方式传递删除器。
如 shared_ptr<int> sp(new int[3], [](int* p)->void{delete[] p; });

但是因为技术问题,仅能简单模拟shared_ptr的类模板形式的删除器

 

template<class T>
struct DeleteArray
{
    void operator()(T* p)
    {
        delete[] p;
    }
};
template<>
struct DeleteArray<string>
{
    void operator()(string* p)
    {
        cout << "Never Give up" << endl;
        delete[] p;
    }
};
template<class T>
struct Free
{
    void operator()(T* p)
    {
        free(p);
    }
};

struct Close
{
    void operator()(FILE* fp)
    {
        fclose(fp);
    }
};

void test()
{
    shared_ptr<int> sp(new int[3], [](int* p)->void{delete []p;});
    shared_ptr<string> sp2(new string[3], [](string* p){delete[] p;});
    shared_ptr<FILE> sp3(fopen("./../hehe.txt", "w+"), [](FILE* p)->void{fclose(p);});
    shared_ptr<int> sp4((int*)malloc(sizeof(int)*5), [](int* p){free(p);});
    unique_ptr<string, DeleteArray<string>> up4(new string[3]);
    unique_ptr<FILE, Close> up1(fopen("./../hhhh.txt", "w+"));
}

weak_ptr

weak_ptr就是为了解决shared_ptr的循环引用问题的。此智能指针没有RAII,没有operator* 和 operator->,不参与资源的创建和释放,不增加引用计数。

std::weak_ptr有一个lock成员函数,用于返回与此weak_ptr绑定的shared_ptr。可以借此访问资源。

weak_ptr没有以T*为参数的构造函数,仅能拷贝构造 or 以一个shared_ptr为参数的构造,所以说weak_ptr就是一个辅助型智能指针,不参与资源管理。 


内存泄漏... 略了

shared_ptr还有线程安全问题,略了,之后学了再说

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

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

相关文章

基于 SSM 的 CRM 客户管理系统

1&#xff1a;通过点击【登录】按钮&#xff0c;获取登录的接口地址 2&#xff1a;在UserLoginController类中&#xff0c;增加登录接口 3&#xff1a;首先判断用户名是否存在&#xff0c;其次判断匹配是否匹配 4&#xff1a;处理session问题 代码如下&#xff1a; UserLo…

java计算机毕业设计ssm智能会议室管理系统0v396(附源码、数据库)

java计算机毕业设计ssm智能会议室管理系统0v396&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xf…

TAPD新增需求自动通知飞书

【实现效果&#xff1a;】TAPD新增需求/缺陷&#xff0c;可以自动通知飞书机器人并通知相关人员&#xff0c;让相关人员可以及时关注到新增需求/缺陷并及时处理。 【流程配置】 第一步&#xff1a;打开腾讯云HiFlow模版中心&#xff0c;搜索打开“TAPD需求/项目更新实时通知飞…

C# !(null包容)运算符的使用

总目录 文章目录总目录前言一、!(null包容&#xff09;运算符是什么&#xff1f;二、!(null包容&#xff09;运算符如何使用&#xff1f;1.使用2.扩展-预处理器指令启用或关闭null检查总结前言 本文主要讲解&#xff01;&#xff08;null包容&#xff09;运算符的使用&#xf…

艾美捷科研专用西妥昔单抗Cetuximab相关介绍

西妥昔单抗&#xff08;Cetuximab&#xff09;&#xff0c;商品名尔必得舒&#xff08;Erbitux&#xff09;&#xff0c;是美商英克隆公司和美商百时美施贵宝的专-利药。西妥昔单抗是一种对抗表皮生长因子受体&#xff08;EGFR&#xff09;的单克隆抗体&#xff0c;经美国食品药…

MySQL表的增删查改(下)

作者&#xff1a;~小明学编程 文章专栏&#xff1a;MySQL 格言&#xff1a;目之所及皆为回忆&#xff0c;心之所想皆为过往 今天给大家分享的是增删查改中的一些比较核心的东西同时也是难点&#xff0c;希望能给大家带来一些帮助吧。 目录 数据库的约束 NULL约束 Unique的…

数据结构C语言版 —— 顺序表增删改查实现

文章目录顺序表1. 线性表2. 顺序表3. 顺序表基本概念4. 顺序表实现顺序表初始化顺序表的扩容顺序表的插入顺序表的删除顺序表的查找顺序表的修改顺序表的销毁5. 顺序表总结顺序表 1. 线性表 线性表&#xff1a;线性表是由n个具有相同特性的数据元素组成的序列。线性表是一种在…

【DevOps】总结下容器方式构建Grafana-reporter生成PDF格式报告

目录 Grafana-reporter1、编写Dockerfile2、构建镜像3、运行Grafana-reporter4、在Grafana配置Link即文章:【DevOps】Prometheus+Grafana:生成pdf报表总结下Grafana-reporter镜像打包过程 最终pdf实现效果类似: Grafana-reporter 简介:“A simple http service that gen…

[附源码]Python计算机毕业设计仿咸鱼二手物品交易系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

TCP/IP 网络原理【TCP篇】

&#x1f389;&#x1f389;&#x1f389;写在前面&#xff1a; 博主主页&#xff1a;&#x1f339;&#x1f339;&#x1f339;戳一戳&#xff0c;欢迎大佬指点&#xff01; 目标梦想&#xff1a;进大厂&#xff0c;立志成为一个牛掰的Java程序猿&#xff0c;虽然现在还是一个…

joinquant量化是什么?是主流的量化平台吗?

joinquant量化是什么&#xff1f;我们做量化投资的时候&#xff0c;目前比较流行的平台&#xff0c;我推荐的是这三个&#xff0c;一个是Ricequant&#xff0c;另外一个是JoinQuant&#xff0c;还有一个QUANTOPIAN&#xff0c;国内实际上就是优矿。这几个现在比较流行&#xff…

git分支管理

分支管理&#xff08;拙见&#xff09; 1&#xff0c;首先master分支是最高级别分支。不可编辑 2&#xff0c;创建一个release分支&#xff0c;从master上拉取&#xff0c;用于上线分支 3&#xff0c;创建dev开发分支&#xff0c;从relase分支拉取&#xff0c;如果有开发环境…

裸辞美团花两月吃透这 Java 岗 798 道真题解析,定级阿里 P7

2023 的面试即将到来&#xff0c;大家准备的怎么样了呢&#xff1f;你有没有正在为此而发愁呢&#xff1f;那么一起来看看小编整理的这富含的 15 个互联网大厂 Java 高级工程师核心面试问题整理吧&#xff01;已助我在 2023 年的金三银四跳槽季中拿到阿里 P7. 内容包括&#x…

Stm32旧版库函数9——ADC读取电压值

#include <stm32f10x_lib.h> #include "adc.h" unsigned char i0; u16 ad[3]{0,0,0}; //初始化ADC //这里我们仅以规则通道为例 void Adc_init(void) { //先初始化…

Unity - Baking System - 烘焙失效的问题

文章目录环境问题解决方法环境 unity : 2020.2.5f1, 2020.3.37f1 pipeline : BRP 问题 我之前有去搜索过场景烘焙失效的问题&#xff0c;都是 unity 发包后和 unity Editor 下运行不同&#xff1a;发包后丢失 烘焙效果&#xff0c;一般都是说&#xff1a;shader 中的 Lightma…

【Hive】分隔符 『 单字节分隔符 | 多字节分隔符』

文章目录1. 概述2. 单字节分隔符方法&#xff1a;使用delimited关键字3. 其它复杂情况方式一&#xff1a;写MR程序进行字符替换转为单字节分隔符问题&#xff08;不推荐&#xff09;方式二&#xff1a;自定义InputFormat转为单字节分隔符问题&#xff08;不推荐&#xff09;方式…

矩阵

链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 来源&#xff1a;牛客网 车车的爱之矩阵是一个行数为 n&#xff0c;列数为 m 的矩阵并满足以下条件: 111. 矩阵元素 xij​ 为整数并满足 0<∣xij​∣⩽114514。 222. 对于在矩阵边界的元素&#xff0c;即 iii 为 1 或…

[附源码]Node.js计算机毕业设计非处方药的查询与推荐系统Express

项目运行 环境配置&#xff1a; Node.js最新版 Vscode Mysql5.7 HBuilderXNavicat11Vue。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分离等等。 环境需要 1.运行环境&#xff1a;最好是Nodejs最新版&#xff0c;我…

Flutter - ScrollController 滚动监听及控制

1 ScrollController jumpTo(double offset)、animateTo(double offset,…)&#xff1a;这两个方法用于跳转到指定的位置&#xff0c;它们不同之处在于&#xff0c;后者在跳转时会执行一个动画&#xff0c;而前者不会。 实例 点击按钮返回顶部 ,且按钮在list滑动一定距离后才…

事务·数据库

事务就是一个完整的业务逻辑 举例&#xff1a; a账户转账至b账户&#xff0c;该操作是一个工作单元&#xff0c;要么同时成功要么同时失败&#xff0c;不可再分。 只有DML(INSERT DELETE UPDATE)语句才会有和事务有关系&#xff0c;因为这三个是操作数据库表中数据进行增删改的…