【C++】深入解析C++智能指针:从auto_ptr到unique_ptr与shared_ptr

文章目录

  • 前言:
  • 1. 智能指针的使用及原理
  • 2. C++ 98 标准库中的 auto_ptr:
  • 3. C++ 11 中的智能指针
    • 循环引用:
    • shared_ptr 定制删除器
  • 4. 内存泄漏
  • 总结:

前言:

随着C++语言的发展,智能指针作为现代C++编程中管理动态分配内存的一种重要工具,越来越受到开发者的青睐。智能指针不仅简化了内存管理,还有助于避免内存泄漏等常见问题。本文将深入探讨智能指针的使用及其原理,从C++98标准库中的auto_ptr开始,逐步过渡到C++11中更为强大和灵活的智能指针类型,如unique_ptrshared_ptr。此外,文章还将讨论循环引用问题、内存泄漏的原因及其危害,并提供相应的解决方案。通过本文的学习,读者将能够更好地理解和运用智能指针,编写出更安全、更高效的C++代码。

1. 智能指针的使用及原理

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

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。
// SmartPtr.h
// 使用RAII思想设计的smartPtr类template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}private:T* _ptr;
};int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{ShardPtr<int> sp1(new int);ShardPtr<int> sp2(new int);cout << div() << endl;
}int main()
{try {Func();}catch(const exception& e){cout<<e.what()<<endl;}return 0;
}
//test.cpp
#include <iostream>
#include "SmartPtr.h"
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl; 
}int main()
{try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}

在这里插入图片描述

  • 需要像指针一样的去使用:
// 像指针一样使用
T& operator*()
{return *_ptr;
}T* operator->()
{return _ptr;
}
SmartPtr<int> sp1(new int(1));
SmartPtr<int> sp2(new int(0));
*sp1 += 10;SmartPtr<pair<string, int>> sp3(new pair<string, int>);
sp3->first = "apple";
sp3->second = 1; // 等价于 sp3.opertor->()->second = 1;cout << sp3->first << " " << sp3->second << endl;
  • 智能指针的拷贝问题
// 智能指针的拷贝问题
int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<int> sp2(sp1);return 0;
}

在这里插入图片描述

vector / list.… 需要深拷贝,它们都是利用资源存储数据,资源是自己的。拷贝时,每个对象各自一份资源,各管各的,所以深拷贝。

智能指针 / 迭代器… 期望的是浅拷贝
资源不是自己的,代为持有,方便访问修改数据。他们拷贝的时候期望的指向同一资源,所以浅拷贝。而且智能指针还要负责释放资源。

itertor it = begin();

2. C++ 98 标准库中的 auto_ptr:

auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!

 // 智能指针的拷贝问题
// 1. auto_ptr 管理权转移,被拷贝的对象把资源管理权转移给拷贝对象,导致被拷贝对象悬空
// 注意:在使用auto_ptr 过后不能访问对象,否则就出现空指针了。很多公司禁止使用它,因为他很坑!
int main()
{std::auto_ptr<int> sp1(new int(1));std::auto_ptr<int> sp2(sp1);*sp2 += 10;// 悬空*sp1 += 10;return 0;
}

auto_ptr 的实现:

namespace hd
{template<class T>class auto_ptr {public:// RAIIauto_ptr(T* ptr = nullptr):_ptr(ptr){}// ap2(ap1)auto_ptr(auto_ptr<T>& ap){_ptr = ap._ptr;ap._ptr = nullptr;}~auto_ptr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;                                                             }}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

3. C++ 11 中的智能指针

boost 智能指针
scoped_ptr / scoped_array
shared_ptr / shared_array

C++ 11
unique_ptrscoped_ptr类似的
shared_ptrshared_ptr类似的

unique_ptr
禁止拷贝,简单粗暴,适合于不需要拷贝的场景
在这里插入图片描述
赋值也禁掉了:
在这里插入图片描述
unique_ptr:实现

namespace hd
{template<class T>class unique_ptr {public:// RAIIunique_ptr(T* ptr = nullptr):_ptr(ptr){}// ap2(ap1)unique_ptr(const unique_ptr<T>& ap) = delete;  // 禁掉拷贝构造// 赋值也要禁掉,赋值会生成默认成员函数,浅拷贝,也会出现问题unique_ptr<T>& operator=(const unique_ptr<T>& ap) = delete;~unique_ptr(){if (_ptr) {std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

如果必须要拷贝用shared_ptr:
shared_ptr 允许自由拷贝,使用引用计数解决多次释放的问题

引用计数: 记录有几个对象参与管理这个资源
在这里插入图片描述
shared_ptr 实现:
使用静态成员变量实现。

namespace hd
{template<class T>class shared_ptr {public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr){_count = 1;}// sp(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;++_count;}~shared_ptr(){if (--_count == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;}}int use_count(){return _count;}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;static int _count;};template<class T>int shared_ptr<T>::_count = 0;
}

在这里插入图片描述
中释放了一个资源!
如果使用静态成员属于这个类,属于这个类的所有对象
需求:每个资源配一个引用计数,而不是全部都是一个引用计数!

所以,一个资源配一个引用计数无论多少个对象管理这个资源,只有这一个计数对象!
怎么找到这个引用呢?每个对象存一个指向计数的指针!

namespace hd
{template<class T>class shared_ptr {public:// RAIIshared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);}void release(){// 说明最后一个管理对象析构了,可以释放资源了if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;delete _ptr;delete _pcount;}}// 赋值 sp1 = sp3;shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr) // 避免自己给自己赋值{release();_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);}return *this;}~shared_ptr(){release();}int use_count(){return *_pcount;}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;int* _pcount;};}

在这里插入图片描述

shared_ptr 的缺陷:

// shared_ptr 的缺陷
struct ListNode
{int _val;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;ListNode(int val = 0):_val(val),_next(nullptr),_prev(nullptr){}};int main()
{std::shared_ptr<ListNode> n1(new ListNode(10));std::shared_ptr<ListNode> n2(new ListNode(20));n1->_next = n2;n2->_prev = n1;//delete n1;//delete n2;return 0;
}

循环引用:

  1. 左边的节点,是由右边的节点_prev管着的,_prev析构,引用计数减到 0, 左边的节点就是释放
  2. 右边节点中_prev 什么时候析构呢?右边的节点被delete时,_prev 析构。
  3. 右边节点什么时候delete呢?右边的节点被左边的节点的_next管着的,_next析构,右边的节点就释放了。
  4. _next 什么时候析构呢?_next 是左边节点的成员,左边节点 delete, _next 就析构了
  5. 左边节点什么时候释放呢?回调 1 点 又循环上去了

右边节点释放 -> _prev析构 -> 左边节点的释放 -> _next析构 -> 右边节点释放

所以这是 shared_ptr 特定场景下的缺陷, 只要有两个shared_ptr 互相管理就会出现这样的情况,所以即使用了智能指针,同样可能导致内存的泄漏。

struct ListNode
{int _val;std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;ListNode(int val = 0):_val(val){}};int main()
{std::shared_ptr<ListNode> n1(new ListNode(10));std::shared_ptr<ListNode> n2(new ListNode(20));cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;//delete n1;//delete n2;return 0;
}

在这里插入图片描述
weak_ptr 可以通过不增加引用计数的方式,避免这个问题。(存在单独自己的 引用计数)
weak_ptr 不支持RAII, 不参与资源管理,不支持指针初始化,但是还是能起到指向你的作用
weak_ptr 的实现:

namespace hd
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;}weak_ptr<T>& operator=(const shared_ptr<T>& sp){  _ptr = sp.get(); // 用 get方法调原生指针}// 像指针一样使用T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

shared_ptr 定制删除器

template<class T>
struct DeleteArry
{void operator()(T* ptr){delete[] ptr;}
};// 定制删除器
int main()
{std::shared_ptr<ListNode> p1(new ListNode(10));std::shared_ptr<ListNode[]> p2(new ListNode[10]); // 可以用数组的std::shared_ptr<ListNode> p2(new ListNode[10], DeleteArry<ListNode>()); // 用仿函数的对象去释放!std::shared_ptr<FILE> p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr);  }); // 用lamada表达式也是可以的return 0;
}

在这里插入图片描述

定制删除器实现:

namespace hd
{template<class T>class shared_ptr{public:// function<void(T*)> _del = [](T* ptr) {delete ptr; };template<class D>shared_ptr(T* ptr, D del):_ptr(ptr), _pcount(new int(1)), _del(del){}// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new int(1)){}// sp2(sp1)shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);}// sp1 = sp4// sp4 = sp4;// sp1 = sp2;shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp)if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;// 拷贝时++计数++(*_pcount);}return *this;}void release(){// 说明最后一个管理对象析构了,可以释放资源了if (--(*_pcount) == 0){std::cout << "delete:" << _ptr << std::endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){// 析构时,--计数,计数减到0,release();}int use_count(){return *_pcount;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;std::function<void(T*)> _del = [](T* ptr) {delete ptr; };};}

4. 内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内
存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对
该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现
内存泄漏会导致响应越来越慢,最终卡死。

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;
}

总结:

本文详细介绍了智能指针的概念、使用和原理,从C++98的auto_ptr到C++11的unique_ptrshared_ptr,展示了智能指针在现代C++编程中的应用和发展。我们了解到RAII(资源获取即初始化)的设计模式,它通过将资源管理封装在对象的生命周期中,简化了资源的获取和释放过程。文章还讨论了智能指针的拷贝问题,特别是auto_ptr的缺陷和shared_ptr的循环引用问题,以及如何使用weak_ptr和定制删除器来解决这些问题。

此外,文章还探讨了内存泄漏的概念、原因和危害,以及如何在实际编程中避免这些问题。通过具体的例子和代码,我们学习了如何使用智能指针来管理资源,确保资源在使用完毕后能够被正确释放,从而避免内存泄漏和其他潜在的资源管理问题。

总的来说,智能指针是C++中一个强大的特性,它不仅提高了代码的安全性和效率,还使得资源管理变得更加简单和直观。通过本文的学习,读者应该能够更加自信地在C++项目中使用智能指针,编写出更加健壮和可靠的软件。

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

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

相关文章

【面试干货】猴子吃桃问题

【面试干货】猴子吃桃问题 1、实现思想2、代码实现 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 猴子吃桃问题&#xff1a;猴子第一天摘下若干个桃子&#xff0c;当即吃了一半&#xff0c;还不瘾&#xff0c;又多吃了一个 二天早上又将剩…

牛客小白月赛94 解题报告 | 珂学家 | 茴字有36种写法

前言 很久没写题解了&#xff0c;有幸参加了94小白月赛内测&#xff0c;反馈是很nice&#xff0c;AK场。 争议的焦点在于哪题最难 D题E题(没有F题)F题(没有E题) 你选哪题呢&#xff1f; 题解 欢迎关注 珂朵莉 牛客周赛专栏 珂朵莉 牛客小白月赛专栏 A. 小苯的九宫格 思路…

手机相册的照片彻底删除了怎么恢复?删除照片恢复的5种方法

在数字化时代&#xff0c;手机相册里装满了我们的生活点滴和珍贵回忆。然而&#xff0c;一不小心就可能误删那些意义非凡的照片。别担心&#xff0c;今天小编就给大家介绍5种恢复误删照片的方法&#xff0c;让你的回忆不再丢失&#xff01; 方法一&#xff1a;相册App的“最近删…

Docker Compose使用

Docker-Compose是什么 docker建议我们每一个容器中只运行一个服务,因为doker容器本身占用资源极少&#xff0c;所以最好是将每个服务单独分割开来&#xff0c;但是这样我们又面临了一个问题&#xff1a; 如果我需要同时部署好多个服务&#xff0c;难道要每个服务单独写Docker…

P4097 【模板】李超线段树 / [HEOI2013] Segment 题解

题意 有一个平面直角坐标系&#xff0c;总共 n n n 个操作&#xff0c;每个操作有两种&#xff1a; 给定正整数 x 0 , y 0 , x 1 , y 1 x_0,y_0,x_1,y_1 x0​,y0​,x1​,y1​ 表示一条线段的两个端点。你需要在平面上加入这一条线段&#xff0c;第 i i i 条被插入的线段的标…

Photoshop插件(UXP)编写过程中,如何更新sp-checkbox的选中状态

✨问题说明 sp-checkbox是uxpSpectrum UXP Widgets下的一个小组件&#xff0c;内置样式大概是这样&#xff1a; 那么&#xff0c;如果用js动态的改变选中的状态&#xff0c;应该如何做呢&#xff1f; 如果直接是html来写&#xff1a; <sp-checkbox checked>Checked<…

特斯拉FSD的「端到端」到底能不能成?

引言 近年来&#xff0c;特斯拉的全自动驾驶&#xff08;Full Self-Driving&#xff0c;FSD&#xff09;技术备受关注&#xff0c;尤其是其「端到端」的AI软件框架更是引发了广泛讨论。端到端技术到底是一条正确的路径吗&#xff1f;它能否真正实现完全自动驾驶&#xff1f;本…

Echarts 实现将X轴放在图表顶部并且自动播放展示提示信息内容

文章目录 需求分析效果预览需求 如下图所示,实现柱状图中反转倒着绘制 分析 使用 ECharts 来实现对 Y 轴的倒序排序时,可以通过设置 yAxis 的 inverse 属性为 true 来实现。以下是一个简单的示例,演示了如何使用 ECharts 来创建一个柱状图,并将 Y 轴进行倒序排序:并且…

前缀和算法:提升编程效率的秘密武器(Java版)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能接…

代码审计--一道简单的文件包含题目的多种利用方式

NO.1 传统方法 首先来看下代码 <?php error_reporting(0); if(isset($_GET["file"])){include($_GET["file"]); }else{highlight_file(__FILE__);phpinfo(); } ?>看完代码后再来学习学习函数吧&#xff0c;毕竟菜啊&#xff01;&#xff01;&…

NASA数据集——阿尔法喷气式大气实验甲醛(HCHO)数据

Alpha Jet Atmospheric eXperiment Formaldehyde Data 简介 阿尔法喷气式大气实验甲醛数据 阿尔法喷气式大气实验&#xff08;AJAX&#xff09;是美国国家航空航天局艾姆斯研究中心与 H211, L.L.C. 公司的合作项目&#xff0c;旨在促进对加利福尼亚、内华达和太平洋沿岸地区的…

【NOIP2014普及组复赛】题4:子矩阵

题3&#xff1a;子矩阵 【题目描述】 给出如下定义&#xff1a; 1.子矩阵&#xff1a;从一个矩阵当中选取某些行和某些列交叉位置所组成的新矩阵&#xff08;保持行与列的相对顺序&#xff09;被称为原矩阵的一个子矩阵。 例如&#xff0c;下面左图中选取第 2 、 4 2、4 2、…

vue项目中使用json编辑器

实现效果&#xff1a; 借助插件json-editor-vue3实现效果如图一&#xff0c;如果嫌丑可以通过类名改一下样式如图二。 实现过程&#xff1a; 安装插件&#xff1a;npm install json-editor-vue3 文档链接&#xff1a;GitCode - 开发者的代码家园 <script setup name&quo…

AcWing 3466. 清点代码库(STL:map,vector)

3466. 清点代码库 需要求有几种不同数列&#xff0c;每种有多少个&#xff0c;可以想到用map。它的键是一个数列&#xff0c;可以把它放在vector里。也就是map<vector<int>,int> 要满足要求的输出序列&#xff0c;就要想把它放在其他容器&#xff0c;或数组里&…

Vite + Vue3 部署 GitHub

因为静态资源是可以部署到 GitHub 上&#xff0c;自己顺便学习部署网站 因为我使用的是 Vite 工具&#xff0c;官方有提供相应 Demo 部署静态站点 | Vite 官方中文文档 新建文件夹 .github 然后再建一个文件夹 workflows 新建文件 main.yml 文件 直接使用官方文档 demo #…

如何处理时间序列的缺失数据

您是否应该删除、插入或估算&#xff1f; 世界上没有完美的数据集。每个数据科学家在数据探索过程中都会有这样的感觉&#xff1a; df.info()看到类似这样的内容&#xff1a; 大多数 ML 模型无法处理 NaN 或空值&#xff0c;因此如果您的特征或目标包含这些值&#xff0c;则在…

Java-MySql:JDBC

目录 JDBC概述 JDBC搭建 1、导入mysql开发商提供的jar包 2、注册驱动 3、与数据库连接 注解&#xff1a; Statement&#xff1a; 代码 运行 PreparedStatement&#xff1a; 代码 运行 PreparedStatement和Statement Statement 增 代码 运行 删 代码 运…

九、图形化脚本

多年来&#xff0c; shell脚本一直都被认为是枯燥乏味的。但如果你准备在图形化环境中运行脚本时&#xff0c;就未必如此了。有很多与脚本用户交互的方式并不依赖read和echo语句。 9.1 创建文本菜单 创建交互式shell脚本最常用的方法是使用菜单。提供各种选项可以帮助脚本用户…

AI遇上遥感,未来会怎样?

随着航空、航天、近地空间等多个遥感平台的不断发展&#xff0c;近年来遥感技术突飞猛进。由此&#xff0c;遥感数据的空间、时间、光谱分辨率不断提高&#xff0c;数据量也大幅增长&#xff0c;使其越来越具有大数据特征。对于相关研究而言&#xff0c;遥感大数据的出现为其提…

基于MetaGPT构建LLM多智能体

前言 你好&#xff0c;我是GISer Liu&#xff0c;在上一篇文章中&#xff0c;我们用了两万多字详细拆解了单个Agent的组成&#xff0c;并通过Github Trending订阅智能体理解MetaGPT框架的订阅模块如何解决应用问题&#xff0c;但是对于复杂&#xff0c;并行的任务&#xff0c;单…