C++11 --- 智能指针
序言
在使用 C / C++ 进行编程时,许多场景都需要我们在堆上申请空间,堆内存的申请和释放都需要我们自己进行手动管理。这就存在容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题,这对长期运行的程序来说是致命的!
但在 C++11 中引入了智能指针,使我们将内存的管理交给智能指针。
1. 什么是智能指针
1.1 概念
该指针旨在自动管理动态分配的内存,减少内存泄漏和野指针的问题
。智能指针是模板类,它们的行为类似于指针,但提供了自动的内存管理功能
。
1.2 RAII 思想
智能指针在 C++ 中主要使用了 资源获取即初始化(Resource Acquisition Is Initialization, RAII)
的思想,以及所有权管理的概念。RAII
的核心思想是,资源的分配(获取)和初始化发生在对象的构造期间
,而 资源的释放(清理)则发生在对象的析构期间
。这种方式通过对象的生命周期来自动管理资源,避免了忘记释放资源(如内存泄漏)的问题。
1.3 体现 RAII 思想
在这里为大家举一个简单的例子来体现 RAII 思想
:
这是我们现在的内存管理,我们需要手动释放申请的资源:
int main()
{int* Ptr = new int[5];// Do Otherthing......delete[] Ptr;return 0;
}
现在实现一个简单的智能指针:
template <class T>
class SmartPtr
{
public:SmartPtr(T* Ptr):_Ptr(Ptr){}// Ohther Functions... ~SmartPtr(){std::cout << "Delete Ptr" << std::endl;delete _Ptr;}private:T* _Ptr;
};int main()
{SmartPtr<int> sp(new int(1));return 0;
}
我们将申请的资源交给智能指针为我们管理,当程序结束时,将自动执行析构函数释放资源:
2. 三种主要的智能指针
2.1 unique_ptr
unique_ptr
是一种 独占所有权
的智能指针,意味着 同一时间内只能有一个 unique_ptr 指向给定的对象
,他直接删除了他的拷贝构造和赋值运算符重载:
注意:保留了对将亡值的赋值运算符重载,因为就修改操作后将亡值就释放了,依旧满足独占所有权的特性!
简单使用一下该指针:
void Ptr_1()
{std::unique_ptr<MyClass> up1(new MyClass); up1->DoSomething();std::unique_ptr<MyClass> up2(move(up1)); // 移动构造std::cout << up1 << std::endl; // up1 这时已被悬空
}
程序输出结果:
I am working!
0000000000000000
~MyClass()
该智能指针还是比较简单的,但是一定要注意指针被悬空后的情况!
2.2 shared_ptr
shared_ptr
是一种 共享所有权
的智能指针,允许多个 shared_ptr
实例指向同一个对象。每个 shared_ptr
都有一个与之关联的计数器,称为控制块,用于跟踪有多少个 shared_ptr
实例指向该对象。当最后一个指向对象的 shared_ptr 被销毁或重置时,对象才会被删除。
简单使用一下该指针:
void Ptr_2()
{std::shared_ptr<MyClass> sp1(new MyClass); // 构造 std::shared_ptr<MyClass> sp2(sp1); // 拷贝构造sp2->DoSomething();std::cout << "The use counts = " << sp1.use_count() << std::endl; // 查看计数器
}
程序输出结果:
I am working!
The use counts = 2
~MyClass()
循环引用问题
使用该指针需要注意一个非常特殊的情况,一不小心掉入坑中!这种情况就是循环引用:
struct Node
{Node(int val):_val(val),_next(nullptr),_prev(nullptr){}~Node(){std::cout << "~Node()" << std::endl;} int _val;std::shared_ptr<Node> _prev;std::shared_ptr<Node> _next;
};void Ptr_3()
{std::shared_ptr<Node> sp1(new Node(1));std::shared_ptr<Node> sp2(new Node(2));// 相互指向sp1->_next = sp2;sp1->_prev = sp1;
}
运行程序,我们会发现并没有正常的未释放资源!出现问题的原因,用图来表示:
当我们函数结束时,函数栈帧销毁,这时两个指针对象调用析构函数来释放资源:
这里的析构函数并不会真正意义上调用 delete
,而是减少引用!直到引用计数为 0 才会调用 delete
,所以,这里的资源并没有真正的被释放,因为 next,prev
指针的存在,所以资源并不会被释放!
2.3 weak_ptr
weak_ptr
是一种 不拥有其所指向对象的智能指针
,它主要 用于解决 shared_ptr 之间的循环引用问题
。weak_ptr
必须与 shared_ptr
一起使用,因为它不拥有对象,所以不会增加对象的共享计数。
简单使用一下该指针:
void Ptr_4()
{// std::weak_ptr<int> wp(new int(1)); // 错误的使用方法 weak_ptr 不能直接管理对象的生命周期std::shared_ptr<int> sp(new int(1));std::weak_ptr<int> wp(sp);std::cout << wp.use_count() << std::endl;
}
现在我们使用 weak_ptr
来解决循环引用的问题:
struct Node
{Node(int val):_val(val){}~Node(){std::cout << "~Node()" << std::endl;} int _val;std::weak_ptr<Node> _prev;std::weak_ptr<Node> _next;
};void Ptr_3()
{std::shared_ptr<Node> sp1(new Node(1));std::shared_ptr<Node> sp2(new Node(2));// 相互指向sp1->_next = sp2;sp2->_prev = sp1;
}
将 _prev, _next
修改为 weak_ptr
来代表不进行引用计数的增加,只是简单的指向,资源现在被正常的释放!
2.4 自定义删除器
在智能指针的底层,对于资源的释放,单个就使用 delete
,数组就是使用 delete[]
,大绝大多数场景下都是没问题的。但是,总是有特殊情况:
void Ptr_5()
{std::shared_ptr<FILE> sp(fopen("test.txt", "w"));
}
请问,这个使用 delete
可以删除吗?当然是不可以,有人会觉得,这不是在鸡蛋里挑骨头吗?其实,我们很多时候就是更应该想到极端情况,Bug
不能被消除,但可以被极力避免!我们程序的健壮性,肯定决定了我们运行的稳定性!
这时,我们就可以使用自定义删除器:
我们需要传递一个可调用对象告诉他,该怎么删除。选择很多,包括函数指针,仿函数… 在这里我们选择 lambda
,这就非常的方便!
std::shared_ptr<FILE> sp(fopen("test.txt", "w"), [](FILE* file) {fclose(file); });
3. 简单实现
在这里我们简单实现一个 shared_ptr
:
3.1 构造函数
首先,先介绍三个成员变量:
T* _Ptr;
std::atomic<int>* _RefCounts; // 引用计数(保证原子性)
std::function<void(T*)> _Del; // 自定义删除器
_Ptr
:是我们需要管理的资源_RefCounts
:计数器,记录多少指针指向该资源(本质就是int
,但是支持原子性操作)_Del
:删除器,保证资源正常的释放,有特殊删除需求可传入
一共实现了简单的三个构造函数:
// 构造函数(默认删除器)
SharedPtr(T* Ptr): _Ptr(Ptr), _RefCounts(new std::atomic<int>(1)), _Del([](T* val) { delete val; })
{}// 构造函数(自定义删除器)
template<class D>
SharedPtr(T* Ptr, D Del): _Ptr(Ptr), _RefCounts(new std::atomic<int>(1)), _Del(Del)
{}// 拷贝构造
SharedPtr(const SharedPtr<T>& sp): _Ptr(sp._Ptr), _RefCounts(sp._RefCounts), _Del(sp._Del)
{++(*_RefCounts);
}
3.2 析构函数
该函数在释放资源前需要判断,当引用计数为 0 时才可释放资源,避免正在使用的指针被悬空:
// 当计数置 0 时调用
void destructor()
{_Del(_Ptr);_Ptr = nullptr;delete _RefCounts;_RefCounts = nullptr;
}// 析构函数
~SharedPtr()
{// 引用减少--(*_RefCounts);if (*_RefCounts == 0){destructor();}
}
3.3 赋值运算符重载
赋值运算符重载需要极其注意,在指向其他资源前需要对当前资源释放(引用计数减一,若为 0,才真正释放资源):
void clear()
{// 引用减少--(*_RefCounts);if (*_RefCounts == 0){destructor();}
}SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{if (this != &sp) // 防止自我赋值{clear(); // 释放当前资源_Ptr = sp._Ptr;_RefCounts = sp._RefCounts;_Del = sp._Del; // 复制删除器++(*_RefCounts);}return *this;
}
3.4 其余的运算符重载
此部分为常用的运算符重载:
T& operator* ()
{return *_Ptr;
}T* operator->()
{return _Ptr;
}
4. 总结
在这篇文章中我们首先介绍了智能指针的思想,之后分别介绍了常用的三种智能指针(unique_ptr, shared_ptr, weak_ptr
),最后我们简单的实现了第二个指针,希望大家有所收获!
相关文章:

C++11 --- 智能指针
序言 在使用 C / C 进行编程时,许多场景都需要我们在堆上申请空间,堆内存的申请和释放都需要我们自己进行手动管理。这就存在容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题…...

C#顺序万年历自写的求余函数与周位移算法
static int 返回月的天数(int 年, int 月){return (月 2 ?(((年 % 4 0 && 年 % 100 > 0) || 年 % 400 0) ? 29 : 28) :(((月 < 7 && 月 % 2 > 0) || (月 > 7 && 月 % 2 0)) ? 31 : 30));}static int 返回年总天数(int 年, int 标 …...

【Java并发编程一】八千字详解多线程
目录 多线程基础 1.线程和进程 线程是什么? 为啥要有线程? 进程和线程的区别? Java 的线程 和 操作系统线程 的关系 使用jconsole观察线程 2.创建线程的多种方式 3.Thread类及其常见方法 Thread类的常见构造方法 Thread类的常见属性…...

CentOS 8FTP服务器
FTP(文件传输协议)是一种客户端-服务器网络协议,允许用户在远程计算机之间传输文件。这里有很多可用于Linux的开源FTP服务软件,最流行最常用的FTP服务软件有 PureFTPd, ProFTPD, 和 vsftpd。在本教程中,我们将在CentOS…...

C++ | Leetcode C++题解之第385题迷你语法分析器
题目: 题解: class Solution { public:NestedInteger deserialize(string s) {if (s[0] ! [) {return NestedInteger(stoi(s));}stack<NestedInteger> st;int num 0;bool negative false;for (int i 0; i < s.size(); i) {char c s[i];if …...

【软件设计师真题】第一大题---数据流图设计
解答数据流图的题目关键在于细心。 考试时一定要仔细阅读题目说明和给出的流程图。另外,解题时要懂得将说明和流程图进行对照,将父图和子图进行对照,切忌按照常识来猜测。同时应按照一定顺序考虑问题,以防遗漏,比如可以…...

系统架构的发展历程之模块化与组件化
模块化开发方法 模块化开发方法是指把一个待开发的软件分解成若干个小的而且简单的部分,采用对复杂事物分而治之的经典原则。模块化开发方法涉及的主要问题是模块设计的规则,即系统如何分解成模块。而每一模块都可独立开发与测试,最后再组装…...

基因组学中的深度学习
----/ START /---- 基因组学其实是一门将数据驱动作为主要研究手段的学科,机器学习方法和统计学方法在基因组学中的应用一直都比较广泛。 不过现在多组学数据进一步激增——这个从目前逐渐增多的各类大规模人群基因组项目上可以看出来,这其实带来了新的挑…...

解决老师询问最高分数问题的编程方案
解决老师询问最高分数问题的编程方案 问题分析数据结构选择:线段树线段树的基本操作伪代码伪代码:构建线段树伪代码:更新操作伪代码:查询操作C语言实现代码详细解释在日常教学中,老师经常需要查询某一群学生中的最高分数,并有时会更新某位同学的成绩。为了实现这一功能,…...

com.baomidou.mybatisplus.annotation.DbType 无法引入
com.baomidou.mybatisplus.annotation.DbType 无法引入爆红 解决 解决 ❤️ 3.4.1 是mybatis-plus版本,根据实际的配置→版本一致 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-annotation</artifactId>&…...

从零开始学习JVM(七)- StringTable字符串常量池
1 概述 String应该是Java使用最多的类吧,很少有Java程序没有使用到String的。在Java中创建对象是一件挺耗费性能的事,而且我们又经常使用相同的String对象,那么创建这些相同的对象不是白白浪费性能吗。所以就有了StringTable这一特殊的存在&…...

数据库课程设计mysql
进行 MySQL 数据库课程设计通常包括以下几个步骤,从需求分析到数据库设计和实现。以下是一个常见的流程及要点: 1. 需求分析 首先,明确系统的功能需求。这包括用户需求、业务流程、功能模块等。你需要与相关人员(比如老师、同学…...

AI学习指南深度学习篇-带动量的随机梯度下降法的基本原理
AI学习指南深度学习篇——带动量的随机梯度下降法的基本原理 引言 在深度学习中,优化算法被广泛应用于训练神经网络模型。随机梯度下降法(SGD)是最常用的优化算法之一,但单独使用SGD在收敛速度和稳定性方面存在一些问题。为了应…...

点餐小程序实战教程03创建应用
目录 1 创建应用2 第一部分侧边栏3 第二部分页面功能区4 第三部分大纲树5 第四部分代码区6 第五部分模式切换7 第六部分编辑区域8 第七部分组件区域9 第八部分,发布区域10 第九部分开发调试和高阶配置总结 上一篇我们介绍了如何实现后端API,介绍了登录验…...

鸿蒙自动化发布测试版本app
创建API客户端 API客户端是AppGallery Connect用于管理用户访问AppGallery Connect API的身份凭据,您可以给不同角色创建不同的API客户端,使不同角色可以访问对应权限的AppGallery Connect API。在访问某个API前,必须创建有权访问该API的API…...

力扣9.7
115.不同的子序列 题目 给你两个字符串 s 和 t ,统计并返回在 s 的 子序列 中 t 出现的个数,结果需要对 109 7 取模。 数据范围 1 < s.length, t.length < 1000s 和 t 由英文字母组成 分析 令dp[i][j]为s的前i个字符构成的子序列中为t的前j…...

GPU 带宽功耗优化
移动端GPU 的内存结构: 先简述移动端内存cache结构;上图的UMA结构 on-Chip memory 包括了 L1、L2 cache,非常关键的移动端的 Tiles 也是保存在 on-chip上还包括寄存器文件:提供给每个核心使用的极高速存储。 共享内存(…...

Linux Centos 7网络配置
本步骤基于Centos 7,使用的虚拟机是VMware Workstation Pro,最终可实现虚拟机与外网互通。如为其他发行版本的linux,可能会有差异。 1、检查外网访问状态 ping www.baidu.com 2、查看网卡配置信息 ip addr 3、配置网卡 cd /etc/sysconfig…...

第三天旅游线路规划
第三天:从贾登峪到禾木风景区,晚上住宿贾登峪; 从贾登峪到禾木风景区入口: 1、行程安排 根据上面的耗时情况,规划一天的行程安排如下: 1)早上9:00起床,吃完早饭&#…...

C++第四十七弹---深入理解异常机制:try, catch, throw全面解析
✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】【C详解】 目录 1.C语言传统的处理错误的方式 2.C异常概念 3. 异常的使用 3.1 异常的抛出和捕获 3.2 异常的重新抛出 3.3 异常安全 3.4 异常规范 4.自定义…...

go 和 java 技术选型思考
背景: go和java我这边自身都在使用,感受比较深,java使用了有7年多,go也就是今年开始的,公司需要所以就学了使用,发现这两个语言都很好,需要根据场景选择,我写下我这边的看法。 关于…...

传统CV算法——边缘算子与图像金字塔算法介绍
边缘算子 图像梯度算子 - Sobel Sobel算子是一种用于边缘检测的图像梯度算子,它通过计算图像亮度的空间梯度来突出显示图像中的边缘。Sobel算子主要识别图像中亮度变化快的区域,这些区域通常对应于边缘。它是通过对图像进行水平和垂直方向的差分运算来…...

图像去噪算法性能比较与分析
在数字图像处理领域,去噪是一个重要且常见的任务。本文将介绍一种实验,通过MATLAB实现多种去噪算法,并比较它们的性能。实验中使用了包括中值滤波(MF)、自适应加权中值滤波(ACWMF)、差分同态算法…...

Vision Transformer(ViT)模型原理及PyTorch逐行实现
Vision Transformer(ViT)模型原理及PyTorch逐行实现 一、TRM模型结构 1.Encoder Position Embedding 注入位置信息Multi-head Self-attention 对各个位置的embedding融合(空间融合)LayerNorm & ResidualFeedforward Neural Network 对每个位置上单…...

828华为云征文 | Flexus X实例CPU、内存及磁盘性能实测与分析
引言 随着云计算的普及,企业对于云资源的需求日益增加,而选择一款性能强劲、稳定性高的云实例成为了关键。华为云Flexus X实例作为华为云最新推出的高性能实例,旨在为用户提供更强的计算能力和更高的网络带宽支持。最近华为云828 B2B企业节正…...

FreeRTOS学习笔记(六)队列
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、队列的基本内容1.1 队列的引入1.2 FreeRTOS 队列的功能与作用1.3 队列的结构体1.4 队列的使用流程 二、相关API详解2.1 xQueueCreate2.2 xQueueSend2.3 xQu…...

【Python篇】PyQt5 超详细教程——由入门到精通(中篇一)
文章目录 PyQt5入门级超详细教程前言第4部分:事件处理与信号槽机制4.1 什么是信号与槽?4.2 信号与槽的基本用法4.3 信号与槽的基础示例代码详解: 4.4 处理不同的信号代码详解: 4.5 自定义信号与槽代码详解: 4.6 信号槽…...

LinuxQt下的一些坑之一
我们在使用Qt开发时,经常会遇到Windows上应用正常,但到Linux嵌入式下就会出现莫名奇妙的问题。这篇文章就举例分析下: 1.QPushButton按钮外侧虚线框问题 Windows下QPushButton按钮设置样式正常,但到了Linux下就会有一个虚线边框。…...

Statement batch
我们可以看到 Statement 和 PreparedStatement 为我们提供的批次执行 sql 操作 JDBC 引入上述 batch 功能的主要目的,是加快对客户端SQL的执行和响应速度,并进而提高数据库整体并发度,而 jdbc batch 能够提高对客户端SQL的执行和响应速度,其…...

PPP 、PPPoE 浅析和配置示例
一、名词: PPP: Point to Point Protocol 点到点协议 LCP:Link Control Protocol 链路控制协议 NCP:Network Control Protocol 网络控制协议,对于上层协议的支持,N 可以为IPv4、IPv6…...