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.自定义…...
19c补丁后oracle属主变化,导致不能识别磁盘组
补丁后服务器重启,数据库再次无法启动 ORA01017: invalid username/password; logon denied Oracle 19c 在打上 19.23 或以上补丁版本后,存在与用户组权限相关的问题。具体表现为,Oracle 实例的运行用户(oracle)和集…...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
大数据学习(132)-HIve数据分析
🍋🍋大数据学习🍋🍋 🔥系列专栏: 👑哲学语录: 用力所能及,改变世界。 💖如果觉得博主的文章还不错的话,请点赞👍收藏⭐️留言Ǵ…...
浪潮交换机配置track检测实现高速公路收费网络主备切换NQA
浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求,本次涉及的主要是收费汇聚交换机的配置,浪潮网络设备在高速项目很少,通…...
【从零学习JVM|第三篇】类的生命周期(高频面试题)
前言: 在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。本文会深入探寻类的生命周期,让读者对此有深刻印象。 目录 …...
