【C++学习】C++智能指针:提高代码安全与性能的利器

文章标题

  • 智能指针的提出
  • 智能指针概念及使用
    • RAII
  • 智能指针的原理
  • C++库多种智能指针详解
    • 版本一:std::auto_ptr(C++98)
      • 1. std::auto_ptr 使用
      • 2. std::auto_ptr 原理
      • 3. std::auto_ptr 模拟实现
    • 版本二:unique_ptr (C++11)
      • 1. unique_ptr 的使用
      • 2. unique_ptr 的原理
      • 3. unique_ptr 的模拟实现
    • 版本三: std::shared_ptr(C++11)
  • 循环引用问题
      • weak_ptr的简单实现


智能指针的提出

在上篇所将的C++异常的文章中,有一个这样的场景:在new/malloc一个空间到delete/free释放这段资源之间,抛异常了,抛异常后直接跳到了catch语句,导致后面的delete/free语句没有执行到,会导致内存泄漏问题。
如下面这个场景:

  • 如下代码,当main函数调用func函数时,func函数内部给ptr开辟了一个空间,然后本来是要在func函数结束时将ptr空间释放的,但是中间如果抛了异常,接直接跳到了catch语句,导致ptr空间没有被释放,造成内存泄漏。
void func(){int* ptr = new int(2);   throw("异常");delete ptr;
}
int main()
{try {func();}catch (const char* s){cout << s << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:
    这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条 智能指针 来管理才有保证。
  2. 采用RAII思想或者智能指针来管理资源。

智能指针概念及使用

RAII

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

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

例如下面的例子:

//使用RAII思想设计的Smart_Pointer类
//将开好的资源给Smart_Pointer这个类创建一个对象,该对象里
//面是一个指针,这个对象来管理这个资源,当对象出了作用域,
//自动调用析构函数将资源清理释放,这样就不会有忘记释放资源
//或则上面中间异常导致delete不到的问题。
template<class T>
class Smart_Pointer{      
public:Smart_Pointer(T* ptr):_ptr(ptr){}~Smart_Pointer(){cout << "~Smart_Pointer" << endl;delete _ptr;}
private:T* _ptr;
};void func()
{Smart_Pointer<int> ptr(new int);   throw("异常");
}
int main(){try {func();}catch (const char* s){cout << s << endl;}catch (...){cout << "未知异常" << endl;}return 0;
}

智能指针的原理

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

举个例子:

template<class T>
class Smart_Pointer
{
public:Smart_Pointer(T* ptr):_ptr(ptr){}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~Smart_Pointer(){cout << "~Smart_Pointer" << endl;delete _ptr;}
private:T* _ptr;
};
struct Date{Date(int year, int month, int day):_year(year),_month(month),_day(day){}~Date(){_year = _month = _day = 0;}int _year;int _month;int _day;
};
int main(){Smart_Pointer<int> ptr1(new int(1));cout << *ptr1 << endl;Smart_Pointer<Date> ptr2(new Date(2024, 4, 10));cout << ptr2->_year <<" " << ptr2->_month << " " << ptr2->_day << " " << endl;return 0;
}
//运行结果:
//1
//2024 4 10
//~Smart_Pointer
//~Smart_Pointer

但是,在实际中,我们可能将指针指向另一个空间,或指针间的拷贝,赋值等问题。
那么,智能指针的拷贝是需要深拷贝还是浅拷贝呢?

答案是:浅拷贝,因为智能指针的行为是模拟指针的行为。
之前学习的数据结构:比如list,vector等容器,特点是,利用这些资源存储管理数据,资源是自己的,拷贝时希望资源各自一份,不是同时利用同一份资源。
而智能指针与迭代器类似,资源不是自己的,代管资源,拷贝的时候希望直指向同一块资源;

那么智能指针的拷贝赋值等问题怎么解决呢?那我们就来看看C++中智能指针的历史发展。


C++库多种智能指针详解

  • 智能指针发展历史
  1. C++ 98 中产生了第一个智能指针auto_ptr.
  2. C++ boost给出了更实用的scoped_ptr和shared_ptr和weak_ptr.
  3. C++ TR1,引入了shared_ptr等。不过注意的是TR1并不是标准版。
  4. C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

版本一:std::auto_ptr(C++98)

文档链接:https://legacy.cplusplus.com/reference/memory/auto_ptr/?kw=auto_ptr

1. std::auto_ptr 使用

//包头文件memory
#include <memory>
int main()
{auto_ptr<int> ptr1(new int(10));auto_ptr<int> ptr2(new int(30));ptr1 = ptr2;   cout<<*ptr1<<endl;   //30
}

2. std::auto_ptr 原理

//当我们打印ptr1的内容时,运行报错
int main()
{auto_ptr<int> ptr1(new int(10));auto_ptr<int> ptr2(ptr1);cout << *ptr2 << endl;   //10//cout << *ptr1 << endl;    //会报错
}

在这里插入图片描述

运行调试时发现:

  • ptr2拷贝构造ptr1前:
    在这里插入图片描述

  • ptr2拷贝构造ptr1后
    在这里插入图片描述

  • 原理总结:
    根据调试结果可以分析出:
    std::auto_ptr是将被拷贝对象值拷贝给拷贝对象,然后将被拷贝对象置空处理;

3. std::auto_ptr 模拟实现

C++98版本的库中就提供了auto_ptr的智能指针。
auto_ptr的实现原理:管理权转移的思想,下面简化模拟实现了一份。

template<class T>
class auto_ptr
{
public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& p){_ptr = p._ptr;p._ptr = nullptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~auto_ptr(){cout << "~Smart_Pointer" << endl;delete _ptr;}
private:T* _ptr;
};

版本二:unique_ptr (C++11)

文档链接https://legacy.cplusplus.com/reference/memory/unique_ptr/?kw=unique_ptr
unique_ptr 版本的智能指针解决拷贝问题,简单粗暴,直接禁止拷贝和赋值。

1. unique_ptr 的使用

//包头文件memory
#include <memory>
int main()
{unique_ptr <int> ptr1(new int(10));//unique_ptr <int> ptr2(ptr1);   //编译报错cout << *ptr2 << endl;//cout << *ptr1 << endl;   
}

2. unique_ptr 的原理

原理:将unipue_ptr里面的拷贝构造与赋值运算符直接禁掉;

3. unique_ptr 的模拟实现

template<class T>
class unique_ptr
{
public:unique_ptr(T* ptr):_ptr(ptr){}unique_ptr(unique_ptr<T>& p) = delete;unique_ptr<T>& operator=(unique_ptr<T>& p) = delete;T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~unique_ptr(){cout << "~unique_ptr()" << endl;delete _ptr;}
private:T* _ptr;
};

版本三: std::shared_ptr(C++11)

文档链接https://legacy.cplusplus.com/reference/memory/shared_ptr/?kw=unique_ptr
shared_ptr 允许拷贝。

  1. shared_ptr使用
int main()
{shared_ptr<int> ptr1(new int(10));shared_ptr<int> ptr2(ptr1);   //可拷贝构造shared_ptr<int> ptr3;ptr3 = ptr1;    //可赋值cout << *ptr2 << endl;cout << *ptr1 << endl;cout << *ptr3 << endl;
}
  1. shared_ptr原理

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

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

注意:share_ptr不允许隐式类型转换

思考:为什么不能用静态成员变量来计数?
如图所示:
在这里插入图片描述
解析
当使用静态成员变量来计数,如图所示,开始时ptr1指向的一个空间,当用ptr1拷贝构造或则复制拷贝一个对象(ptr2)时,只需要_count++,当他们中的一个对象析构时,只需要_count–,当_count等于1时,说明只有一个对象管控着这个资源,当这个对象析构的时候,释放这块空间;

用静态成员变量虽然可以解决这种场景,但是当重新构造另一个对象的时候,该对象引用计数应该为1,但是这里却不为一。
所以用静态成员变量计数行不通。

解决方法:
对象里面存储一个int*的指针,构造的时候将这个空间的值初始化为1,当拷贝构造或者析构的时候,只需要++,–即可;
如图:
在这里插入图片描述

  1. shared_ptr模拟实现
template<class T>
class shared_ptr
{
public:shared_ptr(T* ptr=nullptr):_ptr(ptr),_count(new int(1)){}//拷贝构造shared_ptr(shared_ptr<T>& p){assert(_ptr != p._ptr);_ptr = p._ptr;_count = p._count;++(*_count);}//ptr1=ptr2shared_ptr<T>& operator=(shared_ptr<T>& p){//判断自己赋值给自己,或则间接自己给自己赋值if (_ptr != p._ptr){if (--(*_count) == 0){delete _ptr;delete _count;}_ptr = p._ptr;_count = p._count;++(*_count);}return *this;}int getconut(){return *(_count);}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){cout << "~shared_ptr()" << endl;//当引用计数--后为0,则释放if (--(*_count)==0){delete _ptr;delete _count;}}
private:T* _ptr;int* _count;
};

循环引用问题

  1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
  2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
  3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
  4. 也就是说_next析构了,node2就释放了。
  5. 也就是说_prev析构了,node1就释放了。
  6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放。

在这里插入图片描述
解决方案:
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;
时weak_ptr的_next和_prev不会增加node1和node2的引用计数。

weak_ptr的简单实现

代码实现:

template<class T>
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(shared_ptr<T>& p){_ptr = p.get();}shared_ptr<T>& operator=(shared_ptr<T>&  p){_ptr = p.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~weak_ptr(){cout << "~weak_ptr()" << endl;delete _ptr;	}private:T* _ptr;};

本章完~

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

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

相关文章

算法—分治

分而治之:指的是当主问题可以被分解为一个相同次级问题加相同基本问题时&#xff0c;采用这种思想&#xff0c;基本问题指问题规模最小时的情况&#xff0c;次级问题是指主问题的n级降低n-1级的问题。 具体实现&#xff1a;多数采用递归操作分解&#xff0c;然后递归操作&#…

【Linux】编写一个 shell 脚本执行

在Linux中编写和执行脚本相对简单。下面是一个基本的步骤指南&#xff0c;帮助你创建一个简单的bash脚本并运行它&#xff1a; 1. 创建脚本文件 首先&#xff0c;你需要使用文本编辑器创建一个新的文件。这个文件通常会有 .sh 的扩展名&#xff0c;以表明它是一个shell脚本。…

Android 纵向双选日历

这个日历的布局分两部分&#xff0c;一部分是显示星期几的LinearLayout&#xff0c;另外就是一个RecyclerView&#xff0c;负责纵向滚动了。 工具类&#xff1a; implementation com.blankj:utilcode:1.17.3上activity_calendar代码&#xff1a; <?xml version"1.0&…

研究了一款Vue2开发的Markdown编辑器

最近突然喜欢开始写作了&#xff0c;写笔记&#xff0c;写日记&#xff0c;写总结&#xff0c;各种写。所以&#xff0c;想要打造一个自己喜欢的编辑器&#xff0c;于是开始研究。 首先来看看我从Github丄扒拉到的这个开源的代码&#xff1a; 运行起来以后效果是这样的&…

探究C++20协程(1)——C++协程概览

什么是协程&#xff1f; 协程就是一段可以挂起&#xff08;suspend&#xff09;和恢复&#xff08;resume&#xff09;的程序&#xff0c;一般而言&#xff0c;就是一个支持挂起和恢复的函数。 一般情况下&#xff0c;函数一旦开始&#xff0c;就无法暂停。如果一个函数能够暂…

用于扩展Qt自身的插件(下)

扩展Qt自身的插件 引言必须满足项创建插件示例代码生成插件配置加载插件的环境创建使用插件的项目配置库和头文件依赖的步骤:应用程序代码运行结果总结引言 本文继上篇的扩展Qt自身的插件,接着记录Qt自身的插件,只不过本文提及的用于扩展Qt自身的插件是可以在QtCreator的设…

走进MySQL:从认识到入门(针对初学者)

一&#xff0c;引言 MySQL是一款久负盛名且广泛应用的关系型数据库管理系统&#xff0c;自1995年Michael Widenius和David Axmark在瑞典和芬兰发起研发以来&#xff0c;其发展历程可谓辉煌且深远。作为开源软件的代表&#xff0c;MySQL以其卓越的成本效益、高性能及高可靠性赢得…

关于 Amazon DynamoDB 的学习和使用

文章主要针对于博主自己的技术栈&#xff0c;从Unity的角度出发&#xff0c;对于 DynamoDB 的使用。 绿色通道&#xff1a; WS SDK for .NET Version 3 API Reference - AmazonDynamoDBClient Amazon DynamoDB Amazon DynamoDB is a fast, highly scalable, highly available,…

Harmony鸿蒙南向驱动开发-RTC接口使用

功能简介 RTC&#xff08;real-time clock&#xff09;为操作系统中的实时时钟设备&#xff0c;为操作系统提供精准的实时时间和定时报警功能。当设备下电后&#xff0c;通过外置电池供电&#xff0c;RTC继续记录操作系统时间&#xff1b;设备上电后&#xff0c;RTC提供实时时…

【Java SE】11.认识异常

目录 1.异常的概念与体系结构 1.1异常的概念 1.2异常的体系结构 1.3异常的分类 2.异常的处理 2.1防御式编程 2.2异常的抛出 2.3异常的捕获 2.3.1异常声明throws 2.3.2try-catch捕获并处理 2.3.3finally 2.4异常的处理流程 3.自定义异常类 1.异常的概念与体系结构 …

MAC OS关闭SIP(navicat 无法保存密码)

最近安装navicat&#xff08;16.3.7&#xff09;时,安装后无法保存密码,保存密码会报错如下&#xff1a; 因为用的破解版&#xff0c;一开始是打不开的&#xff0c;用自带的修复软件修复后就可以打开了&#xff0c;但是保存密码就会报错&#xff0c;按照网上的一些操作 1、卸载…

蓝桥杯嵌入式(G431)备赛笔记——RTC

// RTC time// 声明一个变量 rtc_tick 用于记录上次 RTC 处理的时间 u32 rtc_tick 0;// 声明结构体变量 D 用于存储 RTC 的日期信息 RTC_DateTypeDef D;// 声明结构体变量 T 用于存储 RTC 的时间信息 RTC_TimeTypeDef T;// RTC_proc 函数&#xff0c;用于处理 RTC 时间 void R…

Github 2024-04-10 C开源项目日报 Top10

根据Github Trendings的统计,今日(2024-04-10统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量C项目10Shell项目1Python项目1Git - 快速、可扩展、分布式的版本控制系统 创建周期:5740 天开发语言:C, Shell协议类型:OtherStar数量:4955…

.NET 设计模式—享元模式(Flyweight Pattern)

简介 享元模式&#xff08;Flyweight Pattern&#xff09;是一种结构型设计模式&#xff0c;它旨在减少系统中相似对象的内存占用或计算开销&#xff0c;通过共享相同的对象来达到节省资源的目的。 享元模式提供了一种高效地共享对象的方式&#xff0c;从而减少了内存占用和提…

vue3-video-play 在安卓上正常播放,在ios上不能播放,问题解决

1.ios上autoplay需要静音&#xff0c;在播放后再打开声音 <vue3videoPlay v-if"!isComponent" v-bind"options" :playsinline"playsinline"></vue3videoPlay>let playsinline computed(() > {if (props.isComponent) {return}o…

【电路笔记】-异或门

异或门 文章目录 异或门1、概述2、数字逻辑异或门3、异或门等效电路异或逻辑函数是一个非常有用的电路,可用于许多不同类型的计算电路。 1、概述 异或门是算术运算中常用的另一种数字逻辑门,因为它可以用来给出两个二进制数的和以及错误检测和纠正电路。 在前面的文章中,我…

Photoshop 2023 中文---创意与设计的新篇章

Photoshop 2023是由Adobe Systems开发和发行的一款强大的图像处理软件&#xff0c;广泛应用于专业摄影师、设计师、艺术家等用户群体。它拥有丰富的功能和工具&#xff0c;可以轻松进行图像编辑、合成、调整和修复等任务。在Photoshop 2023中&#xff0c;智能选择功能得到了升级…

python爬虫-------urllib代理和代理池(第十七天)

&#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; &#x1f388;&#x1f388;所属专栏&#xff1a;python爬虫学习&#x1f388;&#x1f388; ✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天…

Kubernetes(k8s):深入理解k8s中的亲和性(Affinity)及其在集群调度中的应用

Kubernetes&#xff08;k8s&#xff09;&#xff1a;深入理解k8s中的亲和性&#xff08;Affinity&#xff09;及其在集群调度中的应用 1、什么是亲和性&#xff1f;2、节点亲和性&#xff08;Node Affinity&#xff09;2.1 硬性节点亲和性规则&#xff08;required&#xff09;…

paddle实现手写数字模型(一)

参考文档&#xff1a;paddle官网文档环境&#xff1a;Python 3.12.2 &#xff0c;pip 24.0 &#xff0c;paddlepaddle 2.6.0 python -m pip install paddlepaddle2.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple调试代码如下&#xff1a; LeNet.py import paddle import p…