【C++】智能指针

一、为什么要智能指针

下面我们先分析下面这段程序有没有什么内存方面的问题?

int Div(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");elsereturn a / b;
}
void Func()
{// 1、如果 p1 这里 new 抛异常会如何?// 2、如果 p2 这里 new 抛异常会如何?// 3、如果 Div 这里又会抛异常会如何?int* p1 = new int[10];// 如果这里抛异常没有内存泄漏int* p2 = new int[10];// p1内存泄漏int a, b;cin >> a >> b;cout << Div(a, b) << endl;// p1、p2内存泄漏delete[] p1;delete[] p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

二、内存泄漏

内存泄漏及其危害

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

void MemoryLeaks()
{// 1. 内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int)); int* p2 = new int;// 2. 异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.delete[] p3;
}

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

1. RAII

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

对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构时释放资源。我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr
{T* _ptr;public:SmartPtr(T* ptr = nullptr){_ptr = ptr;}~SmartPtr(){if (_ptr){cout << _ptr << endl;// 至于这里如何决定是使用delete还是delete[] -- 定制删除器delete _ptr;}}
};
int Div(int a, int b)
{if (b == 0)throw invalid_argument("除0错误");elsereturn a / b;
}
void Func()
{int* p1 = new int[10];SmartPtr<int> sp1(p1);// 出了作用域sp1调用析构释放资源int* p2 = new int[10];SmartPtr<int> sp2(p2);int a, b;cin >> a >> b;cout << Div(a, b) << endl;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

2. 智能指针的原理

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

template<class T>
class SmartPtr
{
private:T* _ptr;
public:SmartPtr(T* ptr = nullptr){_ptr = ptr;}~SmartPtr(){if (_ptr){//cout << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
};struct Date
{int _year;int _month;int _day;
};
int main()
{SmartPtr<int> sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr<Date> sp2(new Date);// 需要注意的是这里应该是sp2.operator->()->_year = 2023;// 本来应该是sp2->->_year这里语法上为了可读性,省略了一个->sp2->_year = 2023;sp2->_month = 1;sp2->_day = 1;cout << sp2->_year << endl;
}

总结一下智能指针的原理:

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

3. std::auto_ptr

std::auto_ptr,C++98 版本的库中就提供了auto_ptr的智能指针。

auto_ptr 的实现原理:管理权转移的思想,下面简化模拟实现一份 auto_ptr 来了解它的原理

namespace nb
{template<class T>class auto_ptr{private:T* _ptr;public:auto_ptr(T* ptr):_ptr(ptr){ptr = nullptr;}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;// 如果不置空会发生重复析构}auto_ptr<T>& operator=(auto_ptr<T>& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移 ap 中资源到当前对象中_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}~auto_ptr(){if (_ptr){cout << "delete: " << _ptr << endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}};
}int main()
{std::auto_ptr<int> sp1(new int);std::auto_ptr<int> sp2(sp1); // 管理权转移*sp2 = 10;cout << *sp2 << endl;// sp1悬空//cout << *sp1 << endl;// 空指针无法解引用return 0;
}

结论:auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr


4. std::unique_ptr

C++11 中开始提供更靠谱的 unique_ptr

unique_ptr 的实现原理:简单粗暴的防拷贝,下面简化模拟实现一份 unique_ptr 来了解它的原理。

namespace nb
{template<class T>class unique_ptr{private:T* _ptr;public:unique_ptr(T* ptr):_ptr(ptr){ptr = nullptr;}~unique_ptr(){if (_ptr){delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// 防止拷贝unique_ptr(const unique_ptr<T>& uptr) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;};
}
int main()
{nb::unique_ptr<int> sp1(new int);//nb::unique_ptr<int> sp2(sp1); //*sp1 = 10;cout << *sp1 << endl;return 0;
}

5. std::shared_ptr

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

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

例如: 寝室最后走的关空调

  1. shared_ptr 在其内部,给每个资源都维护着一份计数,用来记录该份资源被几个对象共享。
  2. 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了
template<class T>
class shared_ptr
{
private:T* _ptr;// 注意这里不能给int、static intint* _pRefCount;// 指向引用计数的指针mutex* _pmtx;// 互斥量
public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pRefCount(new int(1)),// 初始化给1_pmtx(new mutex){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pRefCount(sp._pRefCount),_pmtx(sp._pmtx){AddRef();// 增加资源的引用计数}// sp1 = sp2shared_ptr<T>& operator=(const shared_ptr<T>& sp){// 下面这种写法可以防止sp1 = sp1这种情况// 但不能防止这种:sp2(sp1); sp1 = sp2;// 所以要判断资源的地址是否一样// if (this != &sp)if (_ptr != sp._ptr){Release();// 检查sp1原来的资源是否要释放// 管理新资源_ptr = sp._ptr;_pRefCount = sp._pRefCount;_pmtx = sp._pmtx;AddRef();// 增加新资源的引用计数}return *this;}void Release(){_pmtx->lock();bool flag = false;if (--(*_pRefCount) == 0 && _ptr){cout << "delete:" << _ptr << endl;delete _ptr;delete _pRefCount;flag = true;}_pmtx->unlock();// 注意锁的释放if (flag){delete _pmtx;}}void AddRef(){_pmtx->lock();++(*_pRefCount);// 如果是int,++后不会改变另一个对象的计数_pmtx->unlock();}int use_count(){return *_pRefCount;}T* get() const{return _ptr;}~shared_ptr(){Release();}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
};

在这里插入图片描述

std::shared_ptr 的线程安全问题:

通过下面的程序我们来测试 shared_ptr 的线程安全问题。需要注意的是shared_ptr 的线程安全分为两方面:

  1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++ 或 - -,这个操作不是原子的,引用计数原来是1,++ 了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以智能指针中引用计数 ++、- - 是需要加锁的,也就是说引用计数的操作是线程安全的
  2. 智能指针管理的资源对象存放在堆上,两个线程同时去访问,会导致线程安全问题
// shared_ptr智能指针是线程安全的吗?
// 是的,引用计数的加减是加锁保护的。但是指向资源不是线程安全的
// 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了
// 引用计数的线程安全问题,是智能指针要处理的
int main()
{nb::shared_ptr<int> sp1(new int);nb::shared_ptr<int> sp2(sp1);nb::shared_ptr<int> sp3(sp1);nb::shared_ptr<int> sp4(new int);nb::shared_ptr<int> sp5(sp4);sp1 = sp1;sp1 = sp2;sp1 = sp4;sp2 = sp4;sp3 = sp4;*sp1 = 2;*sp2 = 3;return 0;
}

在这里插入图片描述
上图拷贝后资源、引用计数和锁的地址都是一样的。

线程安全问题演示:

如果想演示引用计数线程安全问题,就把 AddRef 和 Release 中的锁去掉。

下面演示的是智能指针管理的资源的线程安全问题。演示可能不出现线程安全问题,因为线程安全问题是偶现性问题,main 函数的 n 改大一些概率就变大了,就更容易出现了。

struct Date
{int _year = 0;int _month = 0;int _day = 0;
};
void SharePtrFunc(nb::shared_ptr<Date>& sp, size_t n, mutex& mtx)
{cout << sp.get() << endl;for (size_t i = 0; i < n; ++i){// 这里智能指针拷贝会++计数,智能指针析构会--计数,这里是线程安全的。nb::shared_ptr<Date> copy(sp);// 这里智能指针访问管理的资源,不是线程安全的。// 理论上这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n// 加个{}是为了让锁出了作用域自动销毁{// 不加锁不能保证智能指针管理的资源是线程安全的unique_lock<mutex> lk(mtx);copy->_year++;copy->_month++;copy->_day++;}}
}
int main()
{nb::shared_ptr<Date> p(new Date);cout << p.get() << endl;const size_t n = 100000;mutex mtx;thread t1(SharePtrFunc, std::ref(p), n, std::ref(mtx));thread t2(SharePtrFunc, std::ref(p), n, std::ref(mtx));t1.join();t2.join();cout << p->_year << " " << p->_month << " " << p->_day << endl;cout << p.use_count() << endl;return 0;
}

在这里插入图片描述

6. std::shared_ptr 的循环引用

struct ListNode
{int _data;shared_ptr<ListNode> _prev;// 智能指针对象管理资源,会自动释放资源shared_ptr<ListNode> _next;~ListNode()// 写析构只是为了看有没有自动调析构{cout << "~ListNode()" << endl;}
};
int main()
{std::shared_ptr<ListNode> node1(new ListNode);std::shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}

输出:
1
1
2
2

main 函数结束后要释放两个节点的空间,但是从输出可以知道智能指针对象的引用计数不会减到0,所以没调析构释放资源。

循环引用分析:

  1. node1 和 node2 两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
  2. node1 的 _next 指向 node2,node2 的 _prev 指向 node1,引用计数变成2。
  3. node1 和 node2 析构,引用计数减到1,但是 node1 的 _next 还指向下一个节点,node2 的 _prev 还指向上 一个节点。
  4. 如果 node1 的 _next 析构了,node2 就释放了。
  5. 如果 node2 的 _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 只指向资源,可以访问资源但不管理资源)。

// 简化版本的 weak_ptr 实现
template<class T>
class weak_ptr
{
private:T* _ptr;
public:// 没有接收指针的构造函数weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
};
struct ListNode
{int _data;weak_ptr<ListNode> _prev;weak_ptr<ListNode> _next;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{shared_ptr<ListNode> node1(new ListNode);shared_ptr<ListNode> node2(new ListNode);cout << node1.use_count() << endl;cout << node2.use_count() << endl;node1->_next = node2;node2->_prev = node1;cout << node1.use_count() << endl;cout << node2.use_count() << endl;return 0;
}

7. 定制删除器

如果不是 new 出来的对象如何通过智能指针管理呢?其实 shared_ptr 设计了一个删除器来解决这个问题

在这里插入图片描述

// 仿函数的删除器
template<class T>
struct FreeFunc {void operator()(T* ptr){cout << "free:" << ptr << endl;free(ptr);}
};
template<class T>
struct DeleteArrayFunc {void operator()(T* ptr){cout << "delete[]" << ptr << endl;delete[] ptr;}
};
class A {};
int main()
{FreeFunc<int> freeFunc;// 构造函数传删除器对象std::shared_ptr<int> sp1((int*)malloc(4), freeFunc);DeleteArrayFunc<int> deleteArrayFunc;std::shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);std::shared_ptr<A> sp4(new A[10], [](A* p) {delete[] p; });std::shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {fclose(p); });return 0;
}

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

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

相关文章

【C++从0到王者】第十六站:stack和queue的使用

文章目录 一、stack的使用1.stack的介绍2.stack的使用 二、queue的使用1.queue的护额晒2.queue的使用 三、stack和queue相关算法题1.最小栈2.栈的压入、弹出序列3.逆波兰表达式4.两个栈实现一个队列5.用两个队列实现栈6.二叉树的层序遍历1.双队列2.用一个变量levelSize去控制 7…

K8S系列文章 之 编写自动化部署K8S脚本

介绍 通过ansible脚本shell实现自动化部署k8s基础集群(v1.25.0) 部署结构 1. 通过二进制部署包镜像安装k8s集群、目录etcd节点只支持1-3个节点、最多三个etcd节点 2. 因k8s版本相对较新、需要升级内核来支持后台程序、当前版本只支持Cento7&#xff0c;内核版本(5.19.4-1.el7…

ffmpeg+nginx实现rtsp协议摄像头web端播放

ffmpegnginx实现rtsp协议摄像头web端播放 环境准备准备nginx环境添加rtmp模块添加hls转发 使用ffmpeg&#xff0c;将摄像头rtsp转为rtmp并推送到nginxVLC播放验证 环境准备 nginx&#xff08;需要安装rtmp模块&#xff09;ffmpeg 6.0vlc播放器&#xff08;本地播放验证&#x…

大数据课程H2——TELECOM的电信流量项目实现

文章作者邮箱&#xff1a;yugongshiyesina.cn 地址&#xff1a;广东惠州 ▲ 本章节目的 ⚪ 了解TELECOM项目的数据收集&#xff1b; ⚪ 了解TELECOM项目的数据清洗&#xff1b; ⚪ 了解TELECOM项目的数据导出&#xff1b; ⚪ 了解TELECOM项目的数据可视化&…

观察者模式(C++)

定义 定义对象间的一种一对多(变化)的依赖关系&#xff0c;以便当一个对象(Subject)的状态发生改变时&#xff0c;所有依赖于它的对象都得到通知并自动更新。 ——《设计模式》GoF 使用场景 一个对象&#xff08;目标对象&#xff09;的状态发生改变&#xff0c;所有的依赖对…

SpringBoot第33讲:SpringBoot集成ShardingJDBC - 基于JPA的读写分离

SpringBoot第33讲&#xff1a;SpringBoot集成ShardingJDBC - 基于JPA的读写分离 本文是SpringBoot第33讲&#xff0c;主要介绍分表分库&#xff0c;以及SpringBoot集成基于 ShardingJDBC 的读写分离实践 文章目录 SpringBoot第33讲&#xff1a;SpringBoot集成ShardingJDBC - 基…

uni-app:实现点击按钮出现底部弹窗(uni.showActionSheet+自定义)

一、通过uni.showActionSheet实现底部选择 效果 代码 <template><view><button click"showActionsheet">点击打开弹窗</button></view> </template><script> export default {methods: {showActionsheet() {uni.showAct…

消息队列项目(2)

我们使用 SQLite 来进行对 Exchange, Queue, Binding 的硬盘保存 对 Message 就保存在硬盘的文本中 SQLite 封装 这里是在 application.yaml 中来引进对 SQLite 的封装 spring:datasource:url: jdbc:sqlite:./data/meta.dbusername:password:driver-class-name: org.sqlite.…

Python 中的机器学习简介:多项式回归

一、说明 多项式回归可以识别自变量和因变量之间的非线性关系。本文是关于回归、梯度下降和 MSE 系列文章的第三篇。前面的文章介绍了简单线性回归、回归的正态方程和多元线性回归。 二、多项式回归 多项式回归用于最适合曲线拟合的复杂数据。它可以被视为多元线性回归的子集。…

APP外包开发的学习流程

学习iOS App的开发是一项有趣和富有挑战性的任务&#xff0c;是一个不断学习和不断进步的过程。掌握基础知识后&#xff0c;不断实践和尝试新的项目将使您的技能不断提升。下面和大家分享一些建议&#xff0c;可以帮助您开始学习iOS App的开发。北京木奇移动技术有限公司&#…

DAY02_Spring—第三方资源配置管理Spring容器Spring注解开发Spring整合Mybatis和Junit

目录 一 第三方资源配置管理1 管理DataSource连接池对象问题导入1.1 管理Druid连接池1.2 管理c3p0连接池 2 加载properties属性文件问题导入2.1 基本用法2.2 配置不加载系统属性2.3 加载properties文件写法 二 Spring容器1 Spring核心容器介绍问题导入1.1 创建容器1.2 获取bean…

软件架构师思维塑造

一、软件系统设计的六项原则 1、单一职责原则&#xff08;Single Responsibility Principle&#xff09; 2、开闭原则&#xff08;Open Closed Principle&#xff09; 3、里氏替换原则&#xff08;Liskov Substitution Principle&#xff09; 4、迪米特法则&#xff08;Law of …

微服务 云原生:基于 Gogs + Drone 进行项目 CI/CD

传统构建部署 以一个简单的前后端项目来说&#xff0c;分别编写前后端的 Dockerfile 文件并构建镜像&#xff0c;然后编写 docker-compose.yml 构建部署&#xff0c;启动运行。 一个简单的例子&#xff1a; 前端&#xff1a; 项目名&#xff1a;kubemanagement-web技术栈&am…

Java中Date方法详解

先进行专栏介绍 本专栏是自己学Java的旅途&#xff0c;纯手敲的代码&#xff0c;自己跟着黑马课程学习的&#xff0c;并加入一些自己的理解&#xff0c;对代码和笔记 进行适当修改。希望能对大家能有所帮助&#xff0c;同时也是请大家对我进行监督&#xff0c;对我写的代码进行…

eeglab(自用)

目录 1.加载、显示数据 2.绘制脑电头皮图 3.绘制通道光谱图 4.预处理工具 5.ICA去除伪迹 5. 提取数据epoch 1.加载、显示数据 观察事件值(Event values)&#xff1a;该数据集中包含2400个事件&#xff0c;每个事件指定了EEG.event结构的字段Type(类型)、position(位置)和…

【Linux命令详解 | cat命令】Linux系统中用于显示或连接文件的命令

文章标题 简介一&#xff0c;参数列表二&#xff0c;使用介绍1. 显示文件内容2. 创建文件3. 连接文件4. 显示行号5. 压缩空行6. 显示特殊字符7. 显示行号和特殊字符8. 从标准输入读取9. 显示文件开头或结尾10. 备份文件11. 显示文件内容至多屏幕大小12. 转义正则表达式13. 显示…

java文件

一.File类 二.扫描指定目录&#xff0c;并找到名称中包含指定字符的所有普通文件&#xff08;不包含目录&#xff09;&#xff0c;并且后续询问用户是否要删除该文件 我的代码: import java.io.File; import java.io.IOException; import java.util.Scanner;public class Tes…

瑞芯微RK3568核心板-4G联网测试

​ &#x1f308;引言 RK3568是瑞芯微针对AIOT和工业市场推出的一款高性能、低功耗、功能丰富的应用处理器。它采用了四核ARM架构64位Cortex-A55处理器&#xff0c;主频高达2.0GHz&#xff0c;集成瑞芯微自研1TOPS算力NPU, 同时集成Mali-G52 2EE GPU&#xff0c;支持4K60fps …

React Native连接Zebra斑马打印机通过发送CPCL指令打印(Android 和 iOS通用)

自 2015 年发布以来&#xff0c;React Native 已成为用于构建数千个移动应用程序的流行跨平台移动开发框架之一。通常&#xff0c;我们有开发人员询问如何将 Link-OS SDK 与 React Native 应用程序集成&#xff0c;以便在 Zebra 打印机上打印标签。在本教程中&#xff0c;我们将…

深度学习常用的激活函数

深度学习的核心思想是通过多层次的神经网络结构&#xff0c;逐步抽取和表示数据中的高级特征&#xff0c;从而实现对复杂数据模式的学习和识别。 神经网络结构&#xff1a; 深度学习使用多层次的神经网络&#xff0c;包括输入层、隐藏层和输出层。这些网络结构允许模型自动学习…