智能指针(C++)

目录

一、智能指针是什么

二、为什么需要智能指针

三、智能指针的使用和原理

3.1、RALL

3.2 智能指针的原理

3.3、智能指针的分类

3.3.1、auto_ptr

3.3.2、unique_ptr

3.3.3、shared_ptr

3.2.4、weak_ptr


一、智能指针是什么

         在c++中,动态内存的管理式通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是极其困难的。有时使用完对象后,忘记释放内存,造成内存泄漏的问题。

        所谓的智能指针本质就是一个类模板,它可以创建任意的类型的指针对象,当智能指针对象使用完后,对象就会自动调用析构函数去释放该指针所指向的空间。

        下面是智能指针的基本框架,所有的智能指针类模板中都需要包含一个指针对象构造函数析构函数

二、为什么需要智能指针

异常的重新抛出的场景:


void File()
{string filename;cin >> filename;FILE* fout = fopen(filename.c_str(), "r");if (fout == nullptr) {string errmsg = "打开文件失败:";errmsg += filename;errmsg += "->";errmsg += strerror(errno);Exception e(errno, errmsg);throw e;}char ch;while ((ch = fgetc(fout))!=EOF) {cout << ch;}fclose(fout);
}double Division(int a, int b)
{if (b == 0) {string errmsg = "Division by zero condition!";Exception e(100, errmsg);throw e;}else{return ((double)a / (double)b);}
}void Func()
{int* p = new int[100];int len, time;cin >> len >> time;try {cout << Division(len,time) << endl;File();}catch (...){//捕获之后,不是要处理异常,异常由最外层同一处理//这里捕获异常只是为了处理内存泄漏的问题delete[]p;throw; }delete[]p;
}int main()
{try {Func();}catch (const Exception& e) {cout << e.what() << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

        在Func函数中,我们在堆上创建了开一个指针,为了防止函数抛出异常导致最后的析构函数不执行而产生野指针,我们使用了 异常的重新抛出策略。

        但是,终究不是个好的方法,如果这类资源较多,那么我们需要大量的 异常重抛 ,而且就算程序不涉及程序处理,大量的堆上空间需要人工释放,容易造成疏漏,这一问题在工程中比较常见。

        所以,这时候如果我们实用智能指针,就可以不用再操心内存是否会泄露的问题。

三、智能指针的使用和原理

3.1、RALL

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

我们可以借助RALL思想来写一个简单的 智能指针:

#include<iostream>
using namespace std;template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;cout<<"~SmartPtr"<<endl;}
private:T* _ptr;
};int main()
{int* a = new int(1);SmartPtr<int> sp(a); //将a 指针委托给sp对象管理SmartPtr<int>sp2(new int(2)); //直接船舰匿名对象给sp2管理
}

3.2 智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可
以通过->去访问所指空间中的内容,因此:AutoPtr模板类中还得需要将* 、->重载下,才可让其
像指针一样去使用。
template<class T>
class SmartPtr 
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}T& operator*() {return *_ptr;}T* operator->() {return _ptr;}
private:T* _ptr;
};struct Date
{int _year;int _month;int _day;
};int main()
{SmartPtr<int> sp1(new int);*sp1 = 10;cout<<*sp1<<endl;SmartPtr<int> sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2018;sparray->_month = 1;sparray->_day = 1;
}
总结一下智能指针的原理:
1. RAII特性
2. 重载operator*和opertaor->,具有像指针一样的行为。

3.3、智能指针的分类

        上面的SmartPtr 还不可以被称为智能指针,因为它还不具有指针的行为与性质。

        指针可以解引用,也可以通过->去访问所指向的空间中的内容,因此智能指针还需要将 *,->重载。

        除此之外,如果我们使用了 拷贝或者赋值操作,就会发生浅拷贝的问题,由于二者指向同一块空间,所以在析构的时候也会析构两次,造成错误。

所以,为了解决以上问题,C++提供了几种设计方案实现的智能指针。

C++中存在4种智能指针:auto_ptr,unquie_ptr,shared_ptr,weak_ptr,他们各有优缺点,以及对应的实用场景

3.3.1、auto_ptr

在C++98版本的库种,提供了 auto_ptr 的智能指针:

class Date
{
public:Date():_year(0),_month(0),_day(0){}~Date(){}int _year;int _month;int _day;};int main()
{auto_ptr<Date>ap(new Date);//拷贝构造auto_ptr<Date>copy(ap);ap->_year = 2022;
}

我们发现报错了,发生了非法访问。

这就是auto_ptr 的弊病,当我们使用对象拷贝或者赋值之后,之前的那个对象就被置空(如下图)

        在拷贝或者赋值的过程种,auto_ptr 会传递所有权,将资源全部从源指针转移给目标指针,源指针被置空。

        虽然这种方法确实解决了 浅拷贝的问题,但是十分局限性也很大,这也就导致了,我们使用auto_ptr的时候要注意,不要对源指针进行访问或者操作。

        由于C++98种提供的这个智能指针问题明显,所以在实际工作种哼多公司是明确规定了不能使用auto_ptr的。

auto_ptr具体实现:

template<class T>class auto_ptr{public:auto_ptr(T* ptr=nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr; //管理权转移}auto_ptr<T>& operator = (auto_ptr<T>& ap){if (this != *ap) {delete _ptr;_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~SmartPtr(){if (_ptr)delete _ptr;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}private:T* _ptr;};

3.3.2、unique_ptr

在C++11中,C++11y引入了unique_ptr.

unique_ptr的原理很简单,就是一个“得不到就毁掉”的理念,直接把拷贝和赋值禁止了。

对于用不上赋值拷贝的场景的时候,我们选择unique_ptr也是一个不错的选择。

template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//防拷贝unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator = (unique_ptr<T>& ap) = delete;~SmartPtr(){if (_ptr)delete _ptr;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}private:T* _ptr;};

3.3.3、shared_ptr

C++中还提供了shared_ptr。

shared_ptr 是当前最为广泛使用的智能指针,它可以安全的提供拷贝操作。

shared_ptr的原理:

        我们可以对一个资源添加一个计数器,让所有管理该资源的智能共用这个计数器,倘若发生拷贝,计数器加一,倘若有析构发生, 计数器减一,当计数器等于0的时候,就把对象析构掉。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

shared_ptr 的模拟实现

template<class T>class shared_ptr{public:shared_ptr(T*ptr =nullptr):_ptr(ptr),_pcount(new int(1)){}//拷贝构造shared_ptr(const T& sp)_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}//赋值拷贝shared_ptr<T>& operator = (shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}~shared_ptr(){if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;}}private:T* _ptr;int* _pcount;};

我们把 这个 计数器 建在堆上,这样就可以保证各个对象之间保持同步同时计数正确。

        拷贝构造

         赋值拷贝

赋值拷贝需要注意两点:

  1. 在被赋值之前的对象需要将自己析构,也就是放弃当前资源的管理权,然后再去被赋值,取得新的管理权。
  2. 避免自己对自己赋值,按照1中的机制,如果自己对自己赋值,会造成无谓的操作,或者误析构资源。

另一种写法:

		shared_ptr<T>& operator=(shared_ptr<T> sp){swap(_ptr, sp._ptr);swap(_pcount, sp._pcount);return *this;}

但是,此时我们的shared_ptr 还面临着 线程安全的问题。

这里我们需要保障的是对于 计数器的 ++ 和 – 造成的线程不安全。对于资源的线程安全问题,这不是智能指针保证的部分

template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)), _pmtx(new mutex){}void add_ref(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release_ref(){bool flag = false;_pmtx->lock();if (--(*_pcount) == 0 && _ptr) {delete _pcount;delete _ptr;flag = true;cout << "释放资源:" << _ptr << endl;}_pmtx->unlock();if (flag)delete _pmtx;}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount),_pmtx(sp._pmtx){add_ref();}shared_ptr<T>& operator = (const shared_ptr<T>& sp){if (_ptr != sp._ptr) {if (--(*_pcount) == 0){delete _pcount;delete _ptr;}_ptr = sp._ptr;_pcount = sp._pcount;add_ref();}return *this;}T& operator *(){return *_ptr;}T* operator ->(){return _ptr;}T* get(){return _ptr;}int use_count(){return *_pcount;}~shared_ptr(){release_ref();}private:T* _ptr;int* _pcount;mutex* _pmtx;};

        定制删除器

        不管是我们自己实现的shared_ptr还是库中的shared_ptr,我们在析构的时候默认都是 delete _ptr,如果我们托管的类型是 new T[] ,或者 malloc出来的话,就导致类型不是匹配的,无法析构。

        为此,shared_ptr提供了 定制删除器,我们可以在构造的时候作为参数传入。如果我们不传参,就默认使用delete

这个自定义删除器可以是函数指针仿函数lamber,包装器。

        仿函数的删除器

shared_ptr中的析构函数会去调用DelArry仿函数去释放动态数组。

3.2.4、weak_ptr

虽然 shared_ptr 确实已经是一个不错的设计了,但是没有“十全十美”的东西,在一些特别的场景之下shared_ptr 也无能为力:

shared_ptr 的循环引用

我们看下面的场景,我们运行发现,两个节点n1.n2 都没有析构。

        在出了作用域之后,首先把 n1,n2 两个对象析构,此时两边计数器均减为1,那么左边节点资源什么时候析构呢, 当n2->prev析构,也就是当右边节点资源析构,那么右边节点资源什么时候析构呢,当n1->_next析构,也就是当左边节点资源析构…我们发现,此时形成了一个类似于“死锁”的情况。

此时我们就要使用 weak_ptr 来解决 循环引用

weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,是为了解决循环引用而生的,为什么这么说呢,我们可以看看它的构造函数:

我们只能使用 wek_ptr或者 shared_ptr 去初始化它。

        我们在会产生循环引用的位置,把shared_ptr换成weak_ptr。 weak_ptr 不是一个RALL智能指针,它不参与资源的管理,他是专门用来解决引用计数的,我们可以使用一个shared_ptr 来初始化一个weak_ptr,但是weak_ptr 不增加引用计数,不参与管理,但是也像指针一样访问修改资源。

        实现一个weak_ptr

template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& sp):_ptr(sp.get()),_pcount(sp.use_count()){}weak_ptr(weak_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){}weak_ptr& operator = (shared_ptr<T>& sp){_ptr = sp.get();_pcount = sp.use_count();return *this;}weak_ptr& operator = (weak_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pcount;}private:T* _ptr;int* _pcount;};

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

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

相关文章

PYCHARM PYSIDE6 QT 打包异常处理 no qt platform plugin could be initialized

安装有PYSIDE6的电脑 异常错误 … no qt platform plugin could be initialized … 变量名&#xff1a;QT_QPA_PLATFORM_PLUGIN_PATH &#xff08;一个字都不能改&#xff01;&#xff01;&#xff09; 自己环境变量值&#xff1a;D:\Users\topma\anaconda3\Lib\site-package…

React中对表格实现列表的拖拽排序

1. 效果:推拽手柄列 2. 实现: react中我们需要两个包来实现 ‘array-move’‘react-sortable-hoc’Installation Use npm $ npm install react-sortable-hoc --save 引入 import { arrayMoveImmutable } from array-move import { SortableContainer, SortableElement, Sort…

Jenkins笔记(一)

个人学习笔记&#xff08;整理不易&#xff0c;有帮助点个赞&#xff09; 笔记目录&#xff1a;学习笔记目录_pytest和unittest、airtest_weixin_42717928的博客-CSDN博客 目录 一&#xff1a;简单了解 二&#xff1a;什么是DevOps 三&#xff1a;安装Jenkins 四&#xff1…

(案例贴2) html+css 倒计时器

欢迎大家使用这个计时器噢 老哥直接附代码咯. timer.html <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0">&l…

GitLab--Merge Request 权限管理

场景 团队在日常开发工作中需要进行分支管理&#xff0c;通常使用feature分支进行开发&#xff0c;然后依次合并到dev分支、release分支&#xff0c;整个代码合并过程不仅仅是代码合并还需要对代码进行审核&#xff0c;如果在线下进行审核合并&#xff0c;这样操作无法保留痕迹…

【力扣hot100】刷题笔记Day18

前言 晚上巩固一下今天的回溯题&#xff0c;基础不牢地动山摇&#xff0c;po一张代码随想录总结的 组合补充 77. 组合 - 力扣&#xff08;LeetCode&#xff09; class Solution:def combine(self, n: int, k: int) -> List[List[int]]:path []res []def backtrack(star…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之FlowItem容器组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之FlowItem容器组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、FlowItem组件 子组件 可以包含子组件。 接口 FlowItem() 使用该接口来…

免费音频剪辑

在数字时代&#xff0c;音频剪辑已成为许多职业和爱好者不可或缺的技能。无论是制作播客、教育视频、还是进行广告宣传&#xff0c;高质量的音频剪辑都能为作品增色不少。今天&#xff0c;我要为大家强烈安利一款免费且功能强大的音频剪辑工具&#xff0c;它绝对是你办公桌上不…

命令行启动mongodb服务器的问题及解决方案 -- Unrecognized option: storage.journal

目录 mongodb命令行启动问题 -- Unrecognized option: storage.journal问题日志&#xff1a;问题截图&#xff1a;问题来源&#xff1a;错误原因&#xff1a;解决方式&#xff1a; mongodb命令行启动问题 – Unrecognized option: storage.journal 同样是格式出问题的问题分析和…

《Spring Security 简易速速上手小册》第5章 高级认证技术(2024 最新版)

文章目录 5.1 OAuth2 和 OpenID Connect5.1.1 基础知识详解OAuth2OpenID Connect结合 OAuth2 和 OIDC 5.1.2 重点案例&#xff1a;使用 OAuth2 和 OpenID Connect 实现社交登录案例 Demo 5.1.3 拓展案例 1&#xff1a;访问受保护资源案例 Demo测试访问受保护资源 5.1.4 拓展案例…

MySQL锁机制【重点】

参考链接 【1】https://xiaolincoding.com/mysql/lock/mysql_lock.html 【2】https://learnku.com/articles/39212?order_byvote_count& 重要的锁&#xff1a; 表级锁&#xff08;Table-level locks&#xff09;&#xff1a; 表级锁是对整个表进行加锁&#xff0c;当一个事…

Blazor 向 ECharts 传递 option

目标 将ECharts封装为Blazor组件&#xff0c;然后通过jsRuntime向ECharts传递参数&#xff0c;即设置option。 封装ECharts 步骤&#xff1a; 1. 在index.html中引入echarts.min.js&#xff1b; 2. 创建blazor组件&#xff0c;将ref传递给js用于初始化echarts&#xff1b; …

#stm学习总结 (二十八)硬件随机数实验

28.1 随机数发生器简介 STM32F407 自带了硬件随机数发生器&#xff08;RNG&#xff09;&#xff0c;RNG 处理器是一个以连续模拟噪声为基础的随机数发生器&#xff0c;在主机读数时提供一个 32 位的随机数。 28.1.1 RNG 框图 STM32F407 的随机数发生器&#xff08;RNG&#x…

ffmpeg单张图片生成固定时长的视频

ffmpeg -r 25 -f image2 -loop 1 -i fps_1.jpg -vcodec libx264 -pix_fmt yuv420p -s 1080*1920 -r 25 -t 30 -y fps.mp4这个命令将 fps_1.jpg 图片转换为一个 30 秒长的视频&#xff0c;分辨率为 1920x1080&#xff0c;帧率为 25 帧/秒&#xff0c;并使用 libx264 编码器进行压…

LeetCode -- 79.单词搜索

1. 问题描述 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中&#xff0c;返回 true &#xff1b;否则&#xff0c;返回 false 。 单词必须按照字母顺序&#xff0c;通过相邻的单元格内的字母构成&#xff0c;其中“相邻”单元格是那些水…

Linux系统——Nginx负载均衡模式

目录 一、Nginx优点 二、Nginx配置项——Conf Upstream 模块 三、Nginx负载均衡 1.负载均衡策略 1.1轮询 1.2IP_hash 1.3URL_hash 1.4Least_conn 1.5Weight 1.6Fair 2.Nginx负载均衡配置状态参数 3.什么是会话保持 3.1会话保持有什么作用呢 3.2Nginx会话保持 3…

《开源软件的影响力》

目录 开源软件的影响力 技术影响力&#xff1a; 经济影响力&#xff1a; 社会影响力&#xff1a; 结论&#xff1a; 开源软件的影响力 简介&#xff1a; 在当今快速发展的科技领域&#xff0c;开源软件已经成为了一种重要的开发模式。本文将重点探讨开源软件对技术、经济和…

用JavaScript动态提取视频中的文字

现阶段整个社会短视频&#xff0c;中视频为王&#xff0c;文字传播虽然被弱化&#xff0c;但在业务中还是有一定的传播价值&#xff0c;今天就来讲一讲如何使用js动态提取视频中的字幕。 先来看看效果&#xff1a; 屏幕录制2024-02-29 15.40.18 一&#xff0c;tesseract.js介…

Android Termux安装MySQL并实现公网远程连接本地数据库

文章目录 前言1.安装MariaDB2.安装cpolar内网穿透工具3. 创建安全隧道映射mysql4. 公网远程连接5. 固定远程连接地址 前言 Android作为移动设备&#xff0c;尽管最初并非设计为服务器&#xff0c;但是随着技术的进步我们可以将Android配置为生产力工具&#xff0c;变成一个随身…

如何解决 C/C++ 编译器优化导致的编译BUG(程序崩溃)支援VC++/CLANG/GCC

本文仅适用于&#xff0c;有愿意、爱捣鼓的童靴。 因编译器优化导致编译BUG&#xff0c;即DEBUG下面无故障稳定工作&#xff0c;但RELESE下程序会在特定函数位置上崩溃。 这要求 C/C 开发人员拥有最基本的素质&#xff0c;需要能够承受&#xff0c;逐行审视编译器输出的目标平…