当前位置: 首页 > news >正文

【C++11】智能指针

📝前言:
这篇文章我们来讲讲C++11——智能指针

🎬个人简介:努力学习ing
📋个人专栏:C++学习笔记
🎀CSDN主页 愚润求学
🌄其他专栏:C语言入门基础,python入门基础,python刷题专栏,Linux


文章目录

  • 一,什么是智能指针
    • 传统指针的问题
    • RAII和智能指针设计思路
  • 二,智能指针的使用
    • (1)C++标准库中的智能指针
      • 总体概述
      • 不同指针的特点
      • delete的问题
        • 删除器提供示例
      • 其他知识
    • (2)C++11和boost库中智能指针的关系
  • 三,智能指针的原理
    • unique_ptr模拟实现
    • shared_ptr模拟实现(重点)
  • 四,shared_ptr和weak_ptr
    • (1)shared_ptr循环引用问题
    • (2)weak_ptr解决问题
        • weak_ptr使用示例
  • 五,share_ptr线程安全问题
  • 六,内存泄漏问题

一,什么是智能指针

传统指针的问题

  • 内存泄漏(忘记delete)
  • 悬垂指针(delete后继续访问)
  • 异常安全问题(抛出异常后,后面的delete语句没办法执行)

为了解决上面的问题就出现了:RAII和智能指针设计思路

RAII和智能指针设计思路

RAII

  • 全称:Resource Acquisition Is Initialization(资源获取即初始化)
  • 是⼀种管理资源的类的设计思想.
  • 本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏。这里的资源可以是内存、⽂件指针、网络连接、互斥锁等等。
    • 对象控制对资源的访问
    • 获取资源与对象的构造绑定
    • 释放资源与对象的析构绑定

智能指针

  • 智能指针就是利用了RAII的设计思想。同时,还要求这个对象能够满足普通指针的行为,方便访问资源。
  • 即:重载 operator*/operator->/operator[] 等运算符,方便访问资源

二,智能指针的使用

(1)C++标准库中的智能指针

总体概述

  • 所在头文件:<memory>
  • 除了weak_ptr他们都符合RAII和像指针⼀样访问的行为。(weak_ptr不能直接管理资源)
  • 不同的指针,区别主要是:解决智能指针拷贝时的思路不同

不同指针的特点

下面讲讲不同的智能指针的特点:

auto_ptr

  • 这是C++98的,是一个糟糕的指针
  • 拷贝时:把被拷贝对象的资源管理权移交给拷贝对象
  • 问题:这样会导致被拷贝对象悬空,后续访问会报错
  • 这指针能不用就不用,C++11也有更好的

unique_ptr

  • 特点:不支持拷贝,只支持移动(传move(ptr)移动构造,移动后,原ptr也会悬空,但是这是用户自己的行为)
  • 如果不需要拷贝的场景就十分建议使用

shared_ptr

  • 支持拷贝,也支持移动
  • 底层通过引用计数实现

weak_ptr

  • 不支持RAII,也就是:不能用它直接管理资源
  • 主要用于:解决shared_ptr的循环引用导致的内存泄漏问题

delete的问题

  • 智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源(如:malloc、new[]、fopen...),交给智能指针管理,析构时就会崩溃

为了解决上面这个问题:

  • 智能指针支持用户提供一个删除器,在删除器中实现资源的释放。
  • 但是提供位置不同
    • unique_ptr作为模板参数提供(建议提供仿函数)
    • shared_ptr作为构造函数参数提供(仿函数、lambda、函数指针等都可以,建议lambda)
  • 删除器的本质是一个可调用对象。当提供了删除器,在智能指针析构的时候,就会调用这个删除器去释放资源。(这个删除器的调用在智能指针析构函数内部)
  • 因为new[]经常使⽤,所以为了简洁⼀点,unique_ptrshared_ptr都特化了⼀份[]的版本
删除器提供示例

示例:

unique_ptr<Date> up1(new Date[5]);

程序崩溃:
在这里插入图片描述
使用特化版本[]的:

unique_ptr<Date[]> up1(new Date[5]);

在这里插入图片描述

shared_ptr的删除器
示例(使用仿函数):

class Del
{
public:void operator()(Date* ptr){delete[] ptr;}
};
// 传入仿函数删除器
shared_ptr<Date> up1(new Date[5], Del());
return 0;

示例(使用lambda):

shared_ptr<Date> up1(new Date[5], [](Date* ptr) {delete[] ptr; });

unique_ptr删除器
示例(使用仿函数):

// 模版参数处提仿函数作为删除器
unique_ptr<Date, Del> up1(new Date[5]);

其他知识

  • shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造

示例(普通:使用指向资源的指针构造):

	shared_ptr<Date> sp1(new Date); // 使用指向Date的指针构造shared_ptr<Date> sp2(sp1); // 用 sp1 拷贝构造 sp2shared_ptr<Date> sp3 = sp2;// 上面这三管理同一块空间

在这里插入图片描述
示例(使用make_shared

	shared_ptr<Date> sp1(new Date);// 使用语法:make_shared<要创建对象的类型>(初始化对象的参数列表)shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造

在这里插入图片描述
可见,管理的是新分配的空间。

  • shared_ptrunique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true

示例(直接把智能指针对象给if判断是否为空):

	shared_ptr<Date> sp2 = make_shared<Date>(2025, 5, 2); // 用初始化对象的值直接构造if (sp2) // 实际上是 if(sp2.operator bool()){cout << "p2不为空" << endl;}
  • shared_ptrunique_ptr 的构造函数都用了 explicit 修饰,防止普通指针隐式类型转换成智能指针对象

(2)C++11和boost库中智能指针的关系

  • Boost 库是 C++ 标准库的扩展程序库集合
  • Boost 库由 Boost 社区的成员进行维护。社区成员贡献了许多 C++ 标准库中未包含的功能和工具(有好有坏)
  • C++ 委员会在制定新的 C++ 标准时,经常参考 Boost 库,取其精华
  • 比如:C ++ 11 引入的unique_ptrshared_ptrweak_ptr。分别是参考boost的scoped_ptrshared_ptrweak_ptr

三,智能指针的原理

unique_ptr模拟实现

unique_ptr的思路是不支持拷贝,只支持移动,在移动的时候,把资源管理权交给另一个指针。

实现代码(不带删除器版本的):

template<class T>
class unique_ptr
{
public:explicit unique_ptr(T* ptr):_ptr(ptr){cout << "init: " << _ptr << endl;}~unique_ptr(){if (_ptr){cout << "delete: " << _ptr << endl;delete _ptr;}}T* operator->(){return _ptr;}T& operator*(){return *_ptr;}// 禁用拷贝构造和拷贝赋值unique_ptr(const unique_ptr<T>& up) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;unique_ptr(unique_ptr<T>&& up):_ptr(up._ptr){up._ptr = nullptr; // 原来的置空,相当于转移了管理权}unique_ptr<T>& operator=(unique_ptr<T>&& up){delete _ptr; // 先释放当前所管理的空间_ptr = up._ptr;up._ptr = nullptr;}private:T* _ptr;
};

shared_ptr模拟实现(重点)

底层利用引用计数器来实现多个指针同时管理。
在这里插入图片描述
模拟实现代码(带删除器)

template<class T>
class shared_ptr
{
public:explicit shared_ptr(T* ptr = nullptr) // 没有删除器的时候,匹配这个:_ptr(ptr), _count(new int(1)){}template<class D>shared_ptr(T* ptr, D del) // 有删除器的时候会匹配这个:_ptr(ptr),_count(new int(1)),_del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_count(sp._count),_del(sp._del){++(*_count);}// 移动构造函数(区别就是_count不变,并且原来的指针要置空)shared_ptr(shared_ptr<T>&& sp): _ptr(sp._ptr), _count(sp._count), _del(move(sp._del)) // 这里也调用del的移动构造函数,提高效率{sp._ptr = nullptr;sp._count = nullptr;}void release() // 不只用于析构,赋值的时候,release也可能要使用{if (_count && --(*_count) == 0){_del(_ptr); // 调用删除器,删除管理的资源delete _count;_ptr = nullptr;_count = nullptr; // 在把自己的资源释放了}}~shared_ptr(){release();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_count = sp._count;_del = sp._del;++(*_count);}return *this;}// 移动赋值shared_ptr<T>& operator=(shared_ptr<T>&& sp){if (this != &sp){release();_ptr = sp._ptr;_count = sp._count;_del = move(sp._del);sp._ptr = nullptr;sp._count = nullptr;}return *this;}T* operator->(){return _ptr;}T& operator*(){return *_ptr;}int use_count() const {return _count ? *_count : 0; // 如果 _count 为空,返回 0}private:T* _ptr;int* _count; function<void(T*)> _del = [](T* ptr) {delete ptr;}; // 删除器,并提供缺省值
};

测试代码:

int main()
{// 构造tr::shared_ptr<Date> sp1(new Date);// 拷贝构造tr::shared_ptr<Date> sp2(sp1);cout << sp2.use_count() << endl;tr::shared_ptr<Date> sp3(new Date);cout << sp3.use_count() << endl;// 拷贝赋值sp3 = sp1;cout << sp1.use_count() << endl;cout << "------------------------------------------------------------" << endl;// 移动构造tr::shared_ptr<Date> sp4(move(sp1));cout << sp4.use_count() << endl;cout << sp1.use_count() << endl; // sp1已经悬空了(不应该继续使用sp1,这里输出0是因为在use_count 里面有判断)// 移动赋值sp4 = move(sp2);cout << sp4.use_count() << endl; // sp2也悬空了,不再管理Date资源cout << "------------------------------------------------------------" << endl;tr::shared_ptr<Date> sp5(new Date[5], [](Date* ptr) {delete[] ptr; });cout << sp5.use_count() << endl;return 0;
}

运行结果:
在这里插入图片描述

四,shared_ptr和weak_ptr

(1)shared_ptr循环引用问题

如果节点有前后指针,prevnext
在这里插入图片描述

  • n1next指向n2的时候,n2的引用计数 +1 == 2
  • n2prev指向n1的时候,n1的引用计数 +1 == 2
  • n1n2析构的时候,引用计数分别-1变成1
  • 但是这时候,n1还依被prev管理,只有prev释放,n1的引用计数才能到0然后被释放
  • prev什么时候被释放呢?只有当n2被释放的时候才被释放,但是n2n1next管理
  • n1next什么时候释放?只有n1被释放才被释放。
  • 所以造成了循环。

(2)weak_ptr解决问题

  • weak_ptr不支持RAII,也不⽀持访问资源,weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数
  • weak_ptr也没有重载operator*operator->等,因为他不参与资源管理。
  • 如果weak_ptr绑定的shared_ptr已经释放了资源(或者指向了别的资源),那么他去访问资源就是很危险的(悬空)
  • expired()可以用来判断weak_ptr绑定的资源是否失效,失效时返回true,没失效返回false
  • lock()weak_ptr绑定的对象还存在的时候,weak_ptr.lock()可以返回⼀个管理资源的shared_ptr,这样weak_ptr就可以访问资源,并且就算原来的shared_ptr改变了,也有这个新的shared_ptr(相当于这就是用weak_ptr创建了一个新的shared_ptr,引用计数会增加)
weak_ptr使用示例

使用示例:

int main()
{shared_ptr<string> sp1(new string("111111"));shared_ptr<string> sp2(sp1);weak_ptr<string> wp = sp1;cout << wp.expired() << endl;cout << wp.use_count() << endl;// sp1和sp2都指向了其他资源,则weak_ptr就过期了sp1 = make_shared<string>("222222");cout << wp.expired() << endl;cout << wp.use_count() << endl;sp2 = make_shared<string>("333333");cout << wp.expired() << endl;cout << wp.use_count() << endl;cout << "-------------------------------------------------" << endl;wp = sp1; // 重新绑定//shared_ptr<string> sp3 = wp.lock(); cout << wp.use_count() << endl;auto sp3 = wp.lock(); // 利用weak_ptr自己创建一个shared_ptrcout << wp.expired() << endl;cout << wp.use_count() << endl;*sp3 += "###";cout << *sp1 << endl;return 0;
}

运行结果:
在这里插入图片描述

五,share_ptr线程安全问题

这里简单讲解一下,暂时不做具体讲解。

  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中同时访问修改引用计数,就会存在线程安全问题。
  • 即:原来是引用计数是1,两个线程同时访问增加:理论上结果应该是1 + 1 + 1 == 3,但是如果两个线程同时拿,拿到的都是 1,则变成两个1 + 1 == 2出现了错误
  • 解决方法:引用计数加锁原子操作atomic<int>
  • 说明一下:智能指针本身是线性安全的,但是指向的资源不是线性安全的。(智能指针指向的资源的线程安全性取决于资源本身的实现,与智能指针无关)

六,内存泄漏问题

内存泄漏

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

内存泄漏的危害

  • 普通程序:运行完就结束。不害怕内存泄漏问题,因为进程正常结束,页表的映射关系解除,物理内存也可以释放。
  • 长期运行的程序:如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

如何检测?

  • 用检测工具

如何预防?

  • 程序员编写代码时,正确使用智能指针,事前预防
  • 事后排错,用检测工具

🌈我的分享也就到此结束啦🌈
要是我的分享也能对你的学习起到帮助,那简直是太酷啦!
若有不足,还请大家多多指正,我们一起学习交流!
📢公主,王子:点赞👍→收藏⭐→关注🔍
感谢大家的观看和支持!祝大家都能得偿所愿,天天开心!!!

http://www.xdnf.cn/news/250669.html

相关文章:

  • 【学习笔记】机器学习(Machine Learning) | 第五章(2)| 分类与逻辑回归
  • 第 12 届蓝桥杯 C++ 青少组中 / 高级组省赛 2021 年真题
  • Python3 基本数据类型
  • Python 常用内置函数详解(八):对象属性操作getattr()、setattr()、delattr()、hasattr()、vars()函数详解
  • 【经管数据】上市公司企业资本要素和劳动要素投入数据(2000-2022年)
  • Memory Bank 不够用?Cline 全新 CRCT:省 token,依赖关系自行追踪
  • 如何解决 H5 远程收款的问题呢?
  • 目标文件的段结构及核心组件详解
  • 多线程系列二:Thread类
  • Window通过虚拟机17安装Ubuntu20.04并安装相关的插件(胎教级教程)
  • 回归树:从原理到Python实战
  • 【C语言】文本操作函数fseek、ftell、rewind
  • 详细介绍Python-pandas-DataFrame全部 功能 函数
  • 存储器层次结构:理解计算机记忆的金字塔
  • 23页PDF | 数据治理实施方案 :规划、执行、评价、改进四步走的管控模式
  • Seata服务端开启事务核心源码解析
  • 位运算题目:寻找重复数
  • 最长公共前缀(14)
  • 基于Koa实现的服务端渲染 ✅
  • 8.进程概念(四)
  • 为什么大模型偏爱Markdown
  • 操作系统(1)多线程
  • 【Machine Learning Q and AI 读书笔记】- 03 小样本学习
  • 数字智慧方案6178丨智慧医院医疗信息化建设之以评促建(61页PPT)(文末有下载方式)
  • 微型计算机串行通信实验三全解析:从原理到实践的探索之旅
  • 《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》章节思维导图
  • 【验证技能】文档要求和好文档注意点
  • Python实现简易博客系统
  • Linux——线程(3)线程同步
  • ✨从噪声到奇迹:扩散模型如何“想象“出世界