C++——探索智能指针的设计原理

        前言: RAII是资源获得即初始化, 是一种利用对象生命周期来控制程序资源地手段。 智能指针是在对象构造时获取资源, 并且在对象的声明周期内控制资源, 最后在对象析构的时候释放资源。注意, 本篇文章参考——C++ 智能指针 - 全部用法详解-CSDN博客

        看完博主的文章的友友们, 可以去看一下该篇文章, 该作者写的比博主通俗易懂。

目录

为什么需要智能指针

智能指针

auto_ptr

auto_ptr的用法:

auto_ptr的模拟实现:

unique_ptr

unique_ptr的用法

unique_ptr的模拟实现

shared_ptr

shared_ptr的用法

 shared_ptr的模拟实现(v1版本)

weak_ptr

weak_ptr的用法:

weak_ptr的模拟实现

shared_ptr中的定制删除器

智能指针坑点 


为什么需要智能指针

        首先我们来看一下这一个简单的程序:


void func() 
{int* p = new int;
}int main() 
{func();return 0;
}

        在这个程序里面, func中定义了一个指向堆区一块空间的p。 但是当出了作用域后, p指针就被销毁了, 但是p指针指向的空间没有被销毁,这个时候就发生了内存泄漏。

        另外一种情况就是我们虽然手动释放了内存, 但是中途发生了异常, 程序发生跳转, 手动释放内存被截胡了。 也会导致发生内存泄漏。

void test() 
{int* ptr = new int;if (1) {throw "发生异常";     //这里发生截胡, 无法走到下一行。}delete ptr;          //这里没有释放资源
}int main() 
{try {test();}catch (const char* str){cout << str << endl;}catch (...) {cout << "未知异常" << endl;}return 0;
}

而只能指针就是为了这种情况设计出来的。也就是说, 智能指针就是为了我们能够方便管理动态内存分配的资源, 它能够在对象的声明周期结束时自动释放这些资源。

        如图为一个简单的智能指针

在这个智能指针当中, 当我们创建对象时,可以使用一块资源初始化。 然后这块资源就会在这个对象的生命周期结束时自动销毁。

        这就是智能指针的基本原理, 虽然我们使用指针时, 指针指向的空间不会被自动释放。但是对象在生命周期结束时会自动释放, 所以我们把指针指向的资源放到对象里, 让对象在释放自身的时候将资源一起释放掉。

智能指针

       

现在有三个智能指针的解决方案:

  •         auto_ptr               C++98
  •         unique_ptr           C++11
  •         share_ptr             C++11

另外, 还有一个用来解除share_ptr中的循环引用问题的解决方案。

  •         wake_ptr               C++11

auto_ptr

auto_ptr的用法:

        使用智能指针需要包含头文件memory, 具体使用方法如下:

#include<memory>             //只用智能指针需要包含memory头文件int main() 
{auto_ptr<int> p(new int);                  //利用auto_ptr创建一个管理int指针资源的对象auto_ptr<list<int>> pl(new list<int>);     //利用auto_ptr创建一个管理list<int>类型的指针资源的对象*p = 4;                    //auto_ptr<int>类型也能进行解引用操作(*pl).push_back(16);       //容器的指针, 解引用后就是容器本身。(*pl).push_back(15);(*pl).push_back(14);(*pl).push_back(13);(*pl).push_back(12);cout << *p << endl;                   //打印*pauto it = (*pl).begin();              //pl解引用获得list<int>对象, 可以像使用指针while (it != (*pl).end()) {cout << *it << endl;++it;}return 0;
}

        这里创建智能指针对象是: auto_ptr<类型名> p(new 类型名)  , 这里创建的时候不能使用 ’ = ‘, 只能使用 ' ( ) ';

        auto_ptr是在C++98创建出来的, 但是这个智能指针在之后很少被人用。 因为它有一个弊端, 就是当进行拷贝的时候, 该智能指针管理的资源会被 ”抛弃“, 另一个智能指针进行接收。 也就是如图:

        这个模式存在一些弊端。如果我们使用一个容器进行插入操作的时候,插入操作一定会赋值。 那么赋值就会导致原本智能指针对象中的资源被转移。

        另外, auto_ptr的另一个弊端就是auto_ptr不支持对象数组的操作。所以在C++11出现更好的unique_ptr和share_ptr后,auto_ptr已经很少被使用。 

        auto_ptr有三个常用接口。 get, release, reset。

三个函数的主要功能

1. get()

        get是获取对象中管理的资源:

2.release

        release, 是取消对象中管理的资源:

3.reset

        reset, 是重新分配对象中管理的资源:

auto_ptr的模拟实现:

//首先, 对于智能指针来说,他们的模板类名是这样的:

	template<class T>       //模板类, 可以接收T类型的资源class auto_ptr          //智能指针类名{};

//然后, 在类里面定义资源类型的指针, 用来维护这块资源: 

	template<class T>class auto_ptr{private:T* _ptr;          //T类型资源的指针, 用来维护一块资源};

//所有智能指针的构造都是一样的, 就是使用一块资源交给智能指针里面的指针变量进行维护。

		auto_ptr(T* ptr):_ptr(ptr){}

 //auto_ptr的拷贝构造, 其实就是把一个智能指针管理的资源 “抛弃”,然后另一个智能指针进行接收。 至于这样做的弊端, 上面已经提到过, 这里不赘述。

		auto_ptr(auto_ptr<T>& ptr):_ptr(ptr._ptr) {ptr._ptr = nullptr;}

//最重要的就是智能指针的销毁, 销毁时, 要将管理的资源一块释放掉, 代码如下:

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

//然后智能指针还要像普通指针一样能够进行基本运算——加加, 减减, 解引用等。那么就要重载这些运算符, 如下图:

		T& operator*()           //解引用{return *_ptr;}T* operator->()          //箭头{return _ptr;}auto_ptr<T> operator++() //加加重载{++_ptr;return *this;}

unique_ptr

unique_ptr的用法

unique_ptr相交于auto_ptr更加严谨, 它相对于auto_ptr做了一下改变:
        两块指针不能指向同一块资源(否则在释放空间时多次释放空间报错)。 同时它也不能赋值(注意, 右值可以赋值,但是右值赋值后, 如果该右值为一个左值临时转化的, 那么使用赋值后和auto_ptr的效果一样)

左值赋值:

 右值赋值:

要注意的坑点就是不能一块资源给多个对象赋值, 不然会报错:

auto_ptr也一样

这是unique_ptr和auto_ptr中使用的坑。 后面的share_ptr解决了这个坑点(引用计数)

unique_ptr的模拟实现

有前面auto_ptr的基础, 这里unique_ptr的细节不再讲解, 只讲解重要的部分:

//首先模板类:

	template<class T>class unique_ptr {private:T* _ptr;      //管理资源};

//然后就是主要的地方 , unique_ptr不能进行赋值, 所以要将拷贝构造和赋值重载封起来。

	template<class T>class unique_ptr {public:unique_ptr(T* ptr):_ptr(ptr) {}private:unique_ptr(unique_ptr<T>& ) = delete;                    //将拷贝构造删掉unique_ptr<T>& operator=(unique_ptr<T>& ) = delete;      //将赋值重载删掉private:T* _ptr;};

shared_ptr

shared_ptr的用法

shared_ptr解决了unique_ptr和auto_ptr的排他性, shared_ptr即使多个智能指针指向同一块空间也能正常工作。 

        shared_ptr采用了引用计数, 当一个新的share_ptr指针管理一块资源的时候, 引用计数就+1, 当一个shared_ptr指针过期时, 引用计数就-1。当一块资源的引用计数到0时, 这块资源就可以被释放。

1. use_count() : 获得当前资源的引用计数:

2.conductor: 有多种构造形式——直接赋值一块资源、 赋值一块数组资源、传送定制删除器。

定义如下:

//带有定制删除器

template<class U, class D>shared_ptr(U* p, D del)

//普通构造 

template<class U>shared_ptr(U* p)

//数组构造

shared_ptr<T[]> p(new T[5]{1, 2, 3, 4, 5});      //据说是C++17后支持, 但是vsC++14也能跑

3.make_shared

make_shared可以用来分配一块空间并且初始化这块空间, 效率更加高效。make_shared是一个函数模板, 并且需要指定分配资源的类型, 如图:

 shared_ptr的模拟实现(v1版本)

首先,类名, 解引用之类和其他智能指针相差不大, 这里不做赘述, 然后有区别的就是成员变量以及构造, 拷贝, 赋值。 

        首先看成员变量, 成员变量需要有一块空间来作为引用计数。

	template<class T>class shared_ptr {private:T* _ptr;int* _pcount;};

*_pcount作为引用计数。

//构造

		//引用计数ptrshared_ptr(T* ptr):_ptr(ptr) {*_pcount = 1;}

//拷贝构造

		//引用计数地拷贝构造shared_ptr(shared_ptr<T>& ptr) :_ptr(ptr._ptr),_pcount(ptr._pcount){++(*_pcount);}

//析构

		~shared_ptr() {if (--(*_pcount) == 0) {delete _ptr;delete _pcount;}}

赋值, 这里赋值要先将原本的管理的资源取消托管, 那么引用计数就要减一, 还要判断引用计数是否为0, 为0就要释放资源。

		shared_ptr<T>& operator=(shared_ptr<T>& p) {if (_ptr != p._ptr)          //如果不判断, 当自己给自己赋值的时候, 自己会先将自己的资源释放, 然后就变成了野指针。 自己再给自己赋值一个野指针。 就会报错。{if (--(*_pcount) == 0){delete _ptr;delete _pcount;}//_ptr = p._ptr;_pcount = p._pcount;++(*_pcount);}return *this;}

shared_ptr中的坑

        1. 其实, shared_ptr还有一种情况同样具有排他性, 和unique_ptr、auto_ptr一样, 当没有调用拷贝构造, 而是直接使用构造函数的时候, 引用计数不回加一, 那么就会多次释放资源。 这个无法避免。

        2.第二个坑就是循环引用的问题,为了方便观察, 我们使用我们自己定义的shared_ptr进行测试, 现在看如下一个例子:

#include"share_ptr.h"      //自己写的shared_ptr头文件
struct chicken;            //前置声明struct fish                //定义一个鱼类对象, 里面有一个鸡的智能指针实例
{cws_RAII::shared_ptr<chicken> _chicken;
};struct chicken              //定义一个鸡类对象, 里面有一个鱼的智能指针实例。
{cws_RAII::shared_ptr<fish> _fish;                           
};int main()
{cws_RAII::shared_ptr<fish> f1(new fish);cws_RAII::shared_ptr<chicken> c1(new chicken);(*f1)._chicken = c1;//(*c1)._fish = f1;return 0;
}

 在当前状态下, 我们如果运行程序, f1的资源和c1的资源可以被释放

但是如过图中(*c1)._fish = f1取消注释, 那么f1的资源和c1的资源就不能被释放。 

这是为什么?

其实, 这就是循环引用的问题。 在这里面, 如果只定义了f1和c1. 这个时候是这样的:

但是如果给执行两条赋值语句后, 就变成了这样:

 这个时候当f1和c1的生命周期结束时, f1的_pcount--, c1的_pcount--。 这两个_pcount都只能变成1, 变不成0, 所以不能释放资源。

        所以, 我们在使用shared_ptr时, 要避免交叉赋值的情况。否则会出现内存泄漏。

weak_ptr

weak_ptr是用来解决shared_ptr的循环引用问题。 当两个类需要交叉进行赋值的时候, 类中所定义的智能指针可以使用weak_ptr(原本使用shared_ptr), 因为weak_ptr不会增加资源的引用计数。 

weak_ptr的用法:

weak_ptr本人觉得最重要的一点就是可以和shared_ptr进行相互转化:

int main()
{shared_ptr<fish> f2(new fish);weak_ptr<fish> f3(f2);     //将一个shared_ptr给给f3f3 = f2;                   //将shared_ptr赋值给weak_ptrf2 = f3.lock();            //将一个weak_ptr使用lock接口转化为shared_ptr;                   return 0;
}

同时, 还可以构造出一个空指针:


int main()
{weak_ptr<fish> f3;     //构造一个空指针return 0;
}

但是, wear_ptr不支持解引用以及->

weak_ptr也能使用use_count函数查看引用计数:

weak_ptr的模拟实现

	template<class T>class weak_ptr {public://不支持RAII, 也就是不能初始化管理资源weak_ptr(const shared_ptr<T>& ptr) {_ptr = ptr._ptr;		//这里可以将weak_ptr弄成shared_ptr的友元, 就能访问私有_ptr_pcount = ptr._pcount;}weak_ptr<T>& operator=(const shared_ptr<T>& ptr) {_ptr = ptr._ptr;_pcount = ptr._pcount;return *this;}int use_count() {return (*_pcount);}T* get() {return _ptr;}private:T* _ptr;int* _pcount;};

shared_ptr中的定制删除器

        在shared_ptr中, 可以管理一块连续的数组空间, 也可以管理一个单独的一块空间。 这两种资源类型需要不同的销毁方法。 单独的使用delete, 而数组空间需要使用delete[], 库里面的默认是使用delete, 但是如果我们想使用delete[]来销毁一块数组空间。 或者我们使用shared_ptr管理一块文件, shared_ptr生命周期结束时关闭文件。 那么就需要我们自己传一个定制删除器。 

        定制删除器是放在如图所示红框框的代码块中:

        定制删除器的用法:

如何实现定制删除器?

其实定制删除器就是添加一个模板类, 如图:

 下面是shared_ptr完整的版本(v2)

	template<class T>class shared_ptr {public://引用计数ptrshared_ptr(T* ptr = nullptr):_ptr(ptr) ,_pcount(new int(0)){if (_ptr != nullptr) {*_pcount = 1;}}//引用计数地拷贝构造shared_ptr(shared_ptr<T>& ptr) :_ptr(ptr._ptr),_pcount(ptr._pcount){++(*_pcount);}//添加一个带有定制删除器的构造函数template<class D>     //D就是定制删除器的模板类shared_ptr(T* ptr, D del):_Del(del), _ptr(ptr) {}void destroy() {if (--(*_pcount) == 0) {_Del;      //将定制删除器放在这就好了}}~shared_ptr() {destroy();}//赋值shared_ptr<T>& operator=(shared_ptr<T>& p) {if (_ptr != p._ptr) {destroy();//_ptr = p._ptr;_pcount = p._pcount;++(*_pcount);}return *this;}T* operator->() {return _ptr;}T& operator*() {return *_ptr;}T* _ptr;int* _pcount;function<void(T*)> _Del = [](T*) {delete _Del};};

智能指针坑点 

在看完大佬们的博客之后, 本人也总结了一些智能指针的 “坑点“, 这个坑点其实都是围绕 原生指针 展开的。

第一个:原生指针不能用来初始化智能指针, 否则两个智能指针指向同一块资源, 引用计数不增加。智能指针过期时会报错 

第二个:get获得的原生指针, 不能delete掉, 否则智能指针在过期后还会delete。会报错

第三个:get获得的原生指针,也是原生指针, 不能初始化另一个智能指针。 

第四个:release返回后的原生指针要被delete掉。 否则内存泄漏。

----以上, 就是本篇全部内容。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

已解决问题 | 该扩展程序未列在 Chrome 网上应用店中,并可能是在您不知情的情况下添加的

在Chrome浏览器中&#xff0c;如果你看到“该扩展程序未列在 Chrome 网上应用店中&#xff0c;并可能是在您不知情的情况下添加的”这样的提示&#xff0c;通常是因为该扩展程序没有通过Chrome网上应用店进行安装。以下是解决这个问题的步骤&#xff1a; 解决办法&#xff1a;…

计算机网络知识整理笔记

目录 1.对网络协议的分层&#xff1f; 2.TCP/IP和UDP之间的区别&#xff1f; 3.建立TCP连接的三次握手&#xff1f; 4.断开TCP连接的四次挥手&#xff1f; 5.TCP协议如何保证可靠性传输&#xff1f; 6.什么是TCP的拥塞控制&#xff1f; 7.什么是HTTP协议&#xff1f; 8…

MySQL高级-SQL优化- limit优化(覆盖索引加子查询)

文章目录 0、limit 优化0.1、从表 tb_sku 中按照 id 列进行排序&#xff0c;然后跳过前 9000000 条记录0.2、通过子查询获取按照 id 排序后的第 9000000 条开始的 10 条记录的 id 值&#xff0c;然后在原表中根据这些 id 值获取对应的完整记录 1、上传5个sql文件到 /root2、查看…

【工具推荐】ONLYOFFICE 桌面编辑器 8.1:引入全新功能,提升文档处理体验

ONLYOFFICE 桌面编辑器 8.1 现已发布&#xff1a;功能完善的 PDF 编辑器、幻灯片版式、改进从右至左显示、新的本地化选项等 【工具推荐】ONLYOFFICE 桌面编辑器 8.1&#xff1a;引入全新功能&#xff0c;提升文档处理体验 一、什么是ONLYOFFICE&#xff1f; ONLYOFFICE 是…

PG备份与恢复

一、开启WAL归档 1、创建归档目录 我们除了存储数据目录pgdata之外&#xff0c;还要创建backups&#xff0c;scripts&#xff0c;archive_wals文件 mkdir -p /home/mydba/pgdata/arch mkdir -p /home/mydba/pgdata/scripts mkdir -p /home/mydba/backups chown -R mydba.myd…

API接口知识小结

应用程序接口API&#xff08;Application Programming Interface&#xff09;&#xff0c;是提供特定业务输出能力、连接不同系统的一种约定。这里包括外部系统与提供服务的系统&#xff08;中后台系统&#xff09;或后台不同系统之间的交互点。包括外部接口、内部接口&#xf…

ANSYS Electronics 电磁场仿真工具下载安装,ANSYS Electronics强大的功能和灵活性

ANSYS Electronics无疑是一款在电磁场仿真领域表现卓越的软件工具。它凭借强大的功能和灵活性&#xff0c;帮助用户在产品设计阶段就能精确预测和优化电磁场性能&#xff0c;从而极大地降低了实际测试成本&#xff0c;并显著提升了产品的可靠性。 这款软件不仅在电子设计领域有…

Python | Leetcode Python题解之第204题计数质数

题目&#xff1a; 题解&#xff1a; MX5000000 is_prime [1] * MX is_prime[0]is_prime[1]0 for i in range(2, MX):if is_prime[i]:for j in range(i * i, MX, i):#循环每次增加iis_prime[j] 0 class Solution:def countPrimes(self, n: int) -> int:return sum(is_prim…

如何利用ChatGPT改善日常生活:一个普通人的指南

当你打开 ChatGPT&#xff0c;显现的是一个简洁的聊天界面。 许多人利用 ChatGPT 进行日常对话。 然而&#xff0c;ChatGPT 的功能远不止于此。 对话只是其众多能力中的一种&#xff0c;如果仅将其视为高级版的聊天机器人&#xff0c;那未免低估了它。 AI 在信息处理方面的…

【SpringMVC】_SpringMVC实现留言墙

目录 1. 需求分析 2. 接口定义 2.1 提交留言 2.2 获取全部留言 3. 响应数据 4. 服务器代码 4.1 MessageInfo 文件 4.2 MessageController 文件 5. 前端页面代码 5. 运行测试 1. 需求分析 实现如下页面&#xff1a; 1、输入留言信息&#xff0c;点击提交后&#xff0…

【算法专题--链表】两数相加 -- 高频面试题(图文详解,小白一看就懂!!)

目录 一、前言 二、题目描述 三、解题方法 ⭐双指针 -- 模拟进位 (使用哨兵位头节点) &#x1f95d; 什么是哨兵位头节点&#xff1f; &#x1f347;思路解析 &#x1f34d;案例图解 四、总结与提炼 五、共勉 一、前言 两数相加 这道题&#xff0c;可以说是--…

如何用一个二维码实现企业固定资产管理?

固定资产管理中普遍存在盘点难、家底不清、账实不一致、权责不清晰等问题。如果平时不规范化执行&#xff0c;年终面对上上下下、大大小小、成百上千件物资要进行盘点整理的时候&#xff0c;会是十分痛苦且低效的事情。 今天这篇文章就来给大家推荐几家便宜好用的二维码固定资…

CST--如何在PCB三维模型中自由创建离散端口

在使用CST电磁仿真软件进行PCB的三维建模时&#xff0c;经常会遇到不能自动创建离散端口的问题&#xff0c;原因有很多&#xff0c;比如&#xff1a;缺少元器件封装、开路端口、多端子模型等等&#xff0c;这个时候&#xff0c;很多人会选择手动进行端口创建&#xff0c;但是&a…

Python:探索高效、智能的指纹识别技术(简单易懂)

目录 概括 导入库 函数一 参数&#xff1a; 函数二 函数三 主函数 运行结果 src&#xff1a; model_base 7.bmp ​编辑 总结 概括 指纹识别是一种基于人体生物特征的身份验证技术。它通过捕捉和分析手指上的独特纹路和细节特征&#xff0c;实现高准确度的身份识别。…

镜头下的光学

说实话&#xff0c;当我看到几何光学的内容全是初中的解析几何的时候&#xff0c;我就觉得讲的方式太原始了&#xff0c;而且太过复杂也看不懂。所以我尝试做了数学建模&#xff0c;发现建模之后模型可以解释一些物理现象&#xff0c;也不会有矛盾的地方&#xff0c;那就算过得…

Tips汇总

爬虫领域 1. 加密参数值 Failed to load 在采集时遇到一个加密参数 Token&#xff0c;搜索 Token 的值&#xff0c;如果是从前面包中返回的&#xff0c;那相对比较好解决&#xff0c;因此进行搜索&#xff0c;却没有搜索到&#xff0c;那我就认为它是加密的算法。 这里其实有…

【全球首个开源AI数字人】DUIX数字人-打造你的AI伴侣!

目录 1. 引言1.1 数字人技术的发展背景1.2 DUIX数字人项目的开源意义1.3 DUIX数字人技术的独特价值1.4 本文目的与结构 2. DUIX数字人概述2.1 定义与核心概念2.2 硅基智能与DUIX的关系2.3 技术架构2.4 开源优势2.5 应用场景2.6 安全与合规性 3. DUIX数字人技术特点3.1 开源性与…

学习gateway网关路由时遇到的问题

遇到这个问题先别慌&#xff0c;我们首先要检查是哪里出问题了&#xff0c;从报错信息中我们可以看到&#xff0c;他说 Unable to find GatewayFilterFactory with name -AddRequestHeader 找不到这个路由过滤器&#xff0c;所以导致网关设置失败&#xff0c;从这条信息上我…

我的北航MEM成长之旅

领完毕业证&#xff0c;2年的学业生涯到此结束。为了方便大家理解后续的内容&#xff0c;这里我们先解释下基本信息&#xff0c;比如MEM到底是个啥&#xff1f;以及北航的MEM都学什么&#xff1f; 1 MEM解读 1.1 MEM是什么&#xff1f; MEM是"Master of Engineering Ma…

如何用CSS样式实现一个优雅的渐变效果?

CSS渐变效果 CSS渐变&#xff08;Gradients&#xff09;是一种让两种或多种颜色平滑过渡的视觉效果&#xff0c;广泛应用于网页背景、按钮、边框等&#xff0c;以创造丰富的视觉体验。CSS提供了线性渐变&#xff08;Linear Gradients&#xff09;和径向渐变&#xff08;Radial…