深入解析智能指针:从实践到原理

👦个人主页晚风相伴

👀如果觉得内容对你有所帮助的话,还请一键三连(点赞关注收藏

如果内容有错或者不足的话,还望你能指出。

目录

智能指针的引入

内存泄漏

RAII

智能指针的使用及原理

std::auto_ptr

std::unique_ptr

std::shared_ptr

std::weak_ptr

定制删除器


智能指针的引入

先看一下下面的代码

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

分析上面的代码,我们会发现代码运行时可能会出现以下问题

  1. 如果p1那里new失败而抛异常
  2. 如果P2那里new失败而抛异常
  3. 如果div调用那里发生除0错误而抛异常

在上面代码中如果出现以上抛异常的情况则会导致资源得不到释放从而导致内存泄漏的问题,那么有什么好的办法可以解决吗?

别说还真有一个好办法,那就是使用智能指针。

在C++中,我们经常会使用new和delete来动态分配和释放内存。但是这种手动管理的方式如果我们忘记在最后调用delete或者在delete抛异常的话就很容易出现一些内存泄漏等问题,另外,如果我们多次释放同一块内存或者在释放内存后继续使用这块内存,就会引起运行时错误。所以为了解决这里的这些问题,C++就引入了智能指针的概念。

智能指针的概念

智能指针是一种特殊的数据类型,其目的是管理动态分配的资源,尤其是堆内存。智能指针是用类模板的方式实现的并且使用RAII(资源获取即初始化)技术来确保资源的正确释放,从而避免内存泄漏和野指针的问题。

内存泄漏

 在介绍智能指针之前先来了解一下什么是内存泄漏以及内存泄漏的危害。

内存泄漏是指因为疏忽或者错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该内存的控制,从而造成了内存的浪费。

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

可以下面这段代码直观体会一下内存泄漏的危害

int main()
{char* p = new char[1024 * 1024 * 1024];cout << (void*)p << endl;return 0;
}

在代码没有运行起来时,我的内存是这么多。

在代码运行起来后,一瞬间我的内存就少了1个G。

可见如果代码中有内存泄漏的问题将会发生很严重的后果。

👍RAII

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

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

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

采用RAII的思想来设计一个SmartPtr

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "Delete:" << _ptr << 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()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2(new int);cout << div() << endl;}
int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}

这样上面的抛异常所带来的内存泄漏问题就能迎刃而解了

 

智能指针的使用及原理

在上面利用RAII思想设计的SmartPtr还不能称作上智能指针,因为上面的SmartPtr只是实现了构造和析构函数,还没有做到像一个指针一样支持解引用和->访问。

因此基于这样的需求我们就需要使用operator重载来实现相关功能。

template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr):_ptr(ptr){}~SmartPtr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}//解引用T& operator*(){return *_ptr;}//->引用T* operator->(){return _ptr;}
private:T* _ptr;
};int main()
{SmartPtr<pair<string, int>> sp1(new pair<string, int>("print", 1));cout << sp1->first << ":" << sp1->second << endl;return 0;
}

👊std::auto_ptr

auto_ptr文档介绍

auto_ptr是C++98版本提供的智能指针,但是auto_ptr设计的并不好,导致很多人对它的意见很大以及很多公司都明确规定不去使用它。

auto_ptr的原理其实是使用了一个管理权转移的思想。先来看看库里面auto_ptr是怎么回事,然后我们就自己动手来简单实现一下auto_ptr。

class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};void test_auto_ptr()
{std::auto_ptr<A> ap1(new A);ap1->_a1++;ap1->_a2++;std::auto_ptr<A> ap2(ap1);ap1->_a1++;ap1->_a2++;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;std::auto_ptr<A> ap3(new A);ap2 = ap3;ap2->_a1++;ap2->_a2++;cout << ap2->_a1 << endl;cout << ap2->_a2 << endl;
}

可以看出auto_ptr在进行拷贝时其实是用了一个管理权转移的思想,将ap1对A的管理权转移到了ap2,之后再将ap1置空,所以上面程序再往下运行就会崩溃。它的赋值操作也是采用同样的思想。

 

知道了怎么回事之后下面我们自己就动手实现一下吧

namespace hjx
{//auto_ptr拷贝和赋值时本质就是管理权转移//被拷贝的对象出现悬空问题template<class T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap)//不能是自己{if (_ptr)//判断一下是否为空{cout << "Delete:" << _ptr << endl;//方便观察结果delete _ptr;}_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;}private:T* _ptr;};
}

🔥std::unique_ptr

 unique_ptr文档介绍

 unique_ptr对象拥有对动态分配的内存资源的唯一所有权,不能进行拷贝构造和赋值操作。

 下面就简单模拟实现一下unique_ptr来了解它的原理吧

namespace hjx
{//unique_ptr不支持拷贝和赋值template<class T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}//使用C++11中的delete将拷贝构造和赋值禁掉unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(unique_ptr<T>& up) = delete;	~unique_ptr(){if (_ptr){cout << "Delete:" << _ptr << endl;delete _ptr;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

🔥std::shared_ptr

 shared_ptr文档介绍

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

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

下面我们就简单模拟实现一下shared_ptr吧

namespace hjx
{//shared_ptr采用引用计数的方式template<class T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount){(*_pCount)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;delete _pCount;}}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();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};
}

但是shared_ptr也并不是完美的,它如果碰到循环引用的情况就会出现问题了

class Node
{
public:int _val;std::shared_ptr<Node> _next;std::shared_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;}
};void test_shared_ptr()
{//循环引用问题std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}

从打印结果来看,它并没有完成析构的一个操作

 下面几幅图就很好的诠释了循环引用的问题

❔该如何解决这里的问题呢?那就需要用到下面即将要介绍的weak_ptr了 

🔥std::weak_ptr

 weak_ptr文档介绍

weak_ptr不是常规的智能指针,没有RAII,不支持直接管理资源,它被设计出来主要是和shared_ptr搭配使用,解决循环引用的问题。

使用weak_ptr时不会增加引用计数,所以我们就可以将上面问题中的_prev和_next换成weak_ptr就能解决问题了。

class Node
{
public:int _val;std::weak_ptr<Node> _next;std::weak_ptr<Node> _prev;~Node(){cout << "~Node()" << endl;}
};void test_shared_ptr()
{std::shared_ptr<Node> n1(new Node);std::shared_ptr<Node> n2(new Node);n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;
}

下面就来简单模拟实现一下weak_ptr吧 

namespace hjx
{//weak_ptr是个辅助型智能指针,用来解决shared_ptr循环引用问题template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}//拷贝和赋值不增加引用计数weak_ptr(const weak_ptr<T>& wp):_ptr(wp._ptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}weak_ptr<T>& operator=(const weak_ptr<T>& wp){_ptr = wp._ptr;return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
}

👀定制删除器

当我们动态开辟一块数组空间时(如new int[10]),我们必须得使用delete[]来释放空间,否则程序将会崩溃。

例如下面的例子

class A
{
public:A(){}~A(){cout << "~A()" << endl;}int _a1 = 0;int _a2 = 0;
};int main()
{std::shared_ptr<A> sp2(new A[10]);//程序崩溃return 0;
}

因为shared_ptr默认使用的是delete来释放空间

其实当我们使用new来动态开辟一块数组空间时,会多开辟4个字节的空间用来存放这块空间的大小,而我们的sp2正好是指向的这4个字节的空间,当使用delete来释放空间时sp2不会跳过这4个字节的空间,所以导致类型不匹配,进而导致程序就崩溃了。

 

所以我们的shared_ptr还要设计一个仿函数删除器来解决这里的问题。

    template<class T>struct Delete{void operator()(T* ptr){delete ptr;}};//shared_ptr采用引用计数的方式template<class T, class D = Delete<T>>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_pCount(new int(1)){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pCount(sp._pCount){(*_pCount)++;//共同管理新资源,++计数}void Release(){if (--(*_pCount) == 0){/*cout << "Delete:" << _ptr << endl;//方便观察现象delete _ptr;*/D()(_ptr);delete _pCount;}}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();}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count(){return *_pCount;}T* get()const{return _ptr;}private:T* _ptr;int* _pCount;//用一个指针来记录计数值};//仿函数删除器template<class T>struct DeleteArray{void operator()(T* ptr){//cout << "delete[]" << ptr << endl;//可不打印delete[] ptr;}};template<class T>struct Free{void operator()(T* ptr){//cout << "free[]" << ptr << endl;//可不打印free(ptr);}};

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

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

相关文章

【Linux 系统】多线程(线程控制、线程互斥与同步、互斥量与条件变量)-- 详解

一、线程概念 线程是进程的一个执行分支&#xff0c;是在进程内部运行的一个执行流。下面将从是什么、为什么、怎么办三个角度来解释线程。 1、什么是线程 上面是一张用户级页表&#xff0c;我们都知道可执行程序在磁盘中无非就是代码或数据&#xff0c;更准确点表述&#xff0…

OpenSceneGraph

文章目录 关于 OpenSceneGraphScreenshots - OpenMW 关于 OpenSceneGraph 官网&#xff1a;https://openscenegraph.github.io/openscenegraph.io/github : https://github.com/openscenegraph/OpenSceneGraphClasses : https://podsvirov.github.io/osg/reference/opensceneg…

Linux 文件管理命令Lawk wc comm join fmt

文章目录 2.Linux 文件管理命令2.44 awk&#xff1a;模式匹配语言1&#xff0e;变量2&#xff0e;运算符3&#xff0e;awk 的正则4&#xff0e;字符串函数5&#xff0e;数学函数案例练习 2.45 wc&#xff1a;输出文件中的行数、单词数、字节数案例练习2.46 comm&#xff1a;比较…

【Spring基础】关于Spring IoC的那些事

文章目录 一、如何理解IoC1.1 Spring IOC 概述1.2 IoC 是什么 二、Ioc 配置的方式2.1 xml 配置2.2 Java 配置2.3 注解配置 三、依赖注入的方式3.1 setter方式3.2 构造函数3.3 注解注入 小结 一、如何理解IoC 1.1 Spring IOC 概述 控制反转 IoC(Inversion of Control)是一种设计…

吴恩达机器学习笔记:第 9 周-15 异常检测(Anomaly Detection) 15.3-15.4

目录 第 9 周 15、 异常检测(Anomaly Detection)15.3 算法15.4 开发和评价一个异常检测系统 第 9 周 15、 异常检测(Anomaly Detection) 15.3 算法 在本节视频中&#xff0c;我将应用高斯分布开发异常检测算法。 异常检测算法&#xff1a;对于给定的数据集 x ( 1 ) , x ( 2…

经典网络解读——Efficientnet

论文&#xff1a;EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks&#xff08;2019.5&#xff09; 作者&#xff1a;Mingxing Tan, Quoc V. Le 链接&#xff1a;https://arxiv.org/abs/1905.11946 代码&#xff1a;https://github.com/tensorflow/t…

python安卓自动化pyaibote实践------学习通自动刷课

前言 欢迎来到我的博客 个人主页:北岭敲键盘的荒漠猫-CSDN博客 本文是一个完成一个自动播放课程&#xff0c;避免人为频繁点击脚本的构思与源码。 加油&#xff01;为实现全部电脑自动化办公而奋斗&#xff01; 为实现摆烂躺平的人生而奋斗&#xff01;&#xff01;&#xff…

【全开源】最新恋爱交友脱单盲盒源码

PHP开源版&#xff0c;带扩列付费恋爱定位入群&#xff0c;内有详细安装教程&#xff0c;轻松部署&#xff0c;搭建即可运营&#xff0c;内置永久免费更新地址&#xff0c;后续无忧升级。 程序介绍&#xff1a; 近期爆火的模式&#xff0c;无压力付费交友&#xff0c;由线下摆…

调用WinPE给现有的Windows做一个备份

前言 前段时间有小伙伴问我&#xff1a;如何让给电脑备份系统。 小白直接告诉他&#xff1a;为啥要备份系统呢&#xff1f;直接给电脑创建一个还原点就好了。 Windows还原点创建教程&#xff08;点我跳转&#xff09; 没想到小伙伴的格局比小白大得多&#xff0c;他说&…

C语言----函数

1.函数的概念 函数&#xff1a;founction c语言的程序代码都是函数组成的 c语言中的函数就是一个完成某项特定的任务的一段代码&#xff0c;这段代码有特殊的写法和调用方法 c语言中我们一般见到两种函数&#xff1a; .库函数 .自定义函数 2.库函数 有对应的头文件 #i…

AutoCAD 2025 for mac/win:设计未来,触手可及

在数字化时代&#xff0c;设计不再局限于纸笔之间&#xff0c;而是跃然于屏幕之上&#xff0c;AutoCAD 2025正是这一变革的杰出代表。无论是Mac用户还是Windows用户&#xff0c;AutoCAD 2025都以其卓越的性能和出色的用户体验&#xff0c;成为了CAD设计绘图领域的佼佼者。 Aut…

什么是 Web3 的生成式 AI?

从 Web 1.0 的静态、单向通信到 Web 2.0 的动态、用户驱动的格局&#xff0c;互联网在二十年的时间里经历了一场显着的转变。现在&#xff0c;当我们站在 Web 3.0 时代的边缘时&#xff0c;我们正在见证更具颠覆性的事物的曙光&#xff1a;生成式人工智能 (AI) 融入我们的数字世…