C++内存列传之RAII宇宙:智能指针

C++内存列传之RAII宇宙:智能指针

文章目录

智能指针是 C++ 中用于自动管理动态内存的类模板,它通过 RAII(资源获取即初始化)技术避免手动 new / delete 操作,从而显著减少内存泄漏和悬空指针的风险

1.为什么需要智能指针?

intdiv(){int a, b; cin >> a >> b;if(b ==0)throwinvalid_argument("除0错误");return a / b;}voidFunc(){int* p1 =newint;int* p2 =newint; cout <<div()<< endl;delete p1;delete p2;}intmain(){try{Func();}catch(exception& e){ cout << e.what()<< endl;}return0;}

如果 p1 这里 new 抛异常会如何?

p1 未成功分配,值为 nullptr
函数直接跳转到 catch 块,p2 未分配,无内存泄漏

如果 p2 这里 new 抛异常会如何?

p1 已分配但未释放,导致内存泄漏
函数跳转到 catch 块,p2 未分配,delete p1delete p2 均未执行

如果 div 调用这里又会抛异常会如何?

p1p2 均已分配但未释放,导致双重内存泄漏
函数跳转到 catch 块,打印错误信息(如 “除 0 错误”)

C++ 不像 java 具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11 就推出了智能指针用于自动管理内存

2.智能指针原理

2.1 RAll

template<classT>classSmartPtr{public:SmartPtr(T* ptr =nullptr):_ptr(ptr){}~SmartPtr(){if(_ptr)delete _ptr;}private: T* _ptr;};intmain(){ SmartPtr<int>sp1(newint(1)); SmartPtr<string>sp2(newstring("xxx"));return0;}

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

简单来说,就是把创建的对象给到 SmartPtr 类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放

这种做法有两大好处:

  • 不需要显式地释放资源
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效

2.2 像指针一样使用

都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 -> 去访问所指空间中的内容,因此类中还得需要将 *-> 重载下,才可让其像指针一样去使用

template<classT>classSmartPtr{public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}private: T* _ptr;};

* 重载返回对象,-> 重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文

3.C++11的智能指针

智能指针一般放在 <memery> 文件里,C++11 也参考了第三方库 boost

  1. C++ 98 中产生了第一个智能指针 auto_ptr
  2. C++ boost 给出了更实用的 scoped_ptrshared_ptrweak_ptr
  3. C++ TR1,引入了 shared_ptr 等。不过注意的是 TR1 并不是标准版
  4. C++ 11,引入了 unique_ptrshared_ptrweak_ptr。需要注意的是 unique_ptr对应 boostscoped_ptr。并且这些智能指针的实现原理是参考 boost 中的实现的

3.1 auto_ptr

template<classT>classauto_ptr{public:// RAII// 像指针一样auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// ap3(ap1)// 管理权转移auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ ap._ptr =nullptr;} auto_ptr<T>&operator=(auto_ptr<T>& ap){if(this!=&ap){ _ptr = ap._ptr;// 转移所有权 ap._ptr =nullptr;// 原指针置空}return*this;}private: T* _ptr;};

auto_ptrC++98 就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、= 重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似

因此,auto_ptr 会导致管理权转移,拷贝对象被悬空,auto_ptr 是一个失败设计,很多公司明确要求不能使用 auto_ptr

3.2 unique_ptr

template<classT>classunique_ptr{public:// RAII// 像指针一样unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){ cout <<"delete:"<< _ptr << endl;delete _ptr;} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// ap3(ap1)// 管理权转移// 防拷贝unique_ptr(unique_ptr<T>& ap)=delete; unique_ptr<T>&operator=(unique_ptr<T>& ap)=delete;private: T* _ptr;};

unique_ptr 很简单粗暴,直接禁止了拷贝机制

因此,建议在不需要拷贝的场景使用该智能指针

3.3 shared_ptr

template<classT>classshared_ptr{public:// RAII// 像指针一样shared_ptr(T* ptr =nullptr):_ptr(ptr),_pcount(newint(1)){}~shared_ptr(){if(--(*_pcount)==0){ cout <<"delete:"<< _ptr << endl;delete _ptr;delete _pcount;}} T&operator*(){return*_ptr;} T*operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);} shared_ptr<T>&operator=(const shared_ptr<T>& sp){if(_ptr == sp._ptr)return*this;if(--(*_pcount)==0){delete _ptr;delete _pcount;} _ptr = sp._ptr; _pcount = sp._pcount;++(*_pcount);return*this;}intuse_count()const{return*_pcount;} T*get()const{return _ptr;}private: T* _ptr;int* _pcount;};

C++11 中的智能指针就属 shared_ptr 使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题

类中增加一个新的指针 _pcount 用于计数,即计数有多少个 _ptr 指向同一片空间,多个 shared_ptr 可以同时指向同一个对象,每次创建新的 shared_ptr 指向该对象,引用计数加 1;每次 shared_ptr 析构或者被赋值为指向其他对象,引用计数减 1。当最后一个指向该对象的 shared_ptr 析构时,对象会被自动删除,从而避免内存泄漏

🔥值得注意的是:shared_ptr 同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux 学习过后再来补充

3.4 weak_ptr

看似完美的 shared_ptr 其实也会有疏漏,比如:引用循环

structListNode{int _data; shared_ptr<ListNode> _next; shared_ptr<ListNode> _prev;};intmain(){ shared_ptr<ListNode>node1(new ListNode); shared_ptr<ListNode>node2(new ListNode); cout << node1.use_count()<< endl; cout << node2.use_count()<< endl; node1->_next = node2; node2->_prev = node1; cout << node1.use_count()<< endl; cout << node2.use_count()<< endl;return0;}

当执行 node1->next = node2node2->prev = node1 时,node1 内部的 _next 指针指向 node2node2 内部的 _prev 指针指向 node1 。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2 ,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用

node1node2 智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1 。此时,每个节点的引用计数变为 1 ,而不是预期的 0 。这是因为 node1_next 还指向 node2node2_prev 还指向 node1 ,使得它们的引用计数无法归零

对于 shared_ptr 来说,只有当引用计数变为 0 时才会释放所管理的资源。由于这种循环引用的存在,node1 等待 node2 先释放(因为 node2_prev 引用着 node1 ),而 node2 又等待 node1 先释放(因为 node1_next 引用着 node2 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏

classListNode{public: weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev;};

为了解决 shared_ptr 的循环引用问题,通常可以使用 weak_ptrweak_ptr 是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode 类中的 _prev_next 其中之一)改为 weak_ptr 类型,就可以打破循环引用

因此,weak_ptr 是一种专门解决循环引用问题的指针

4.删除器

#include<iostream>#include<memory>#include<string>usingnamespace std;classA{public:~A(){ cout <<"A::~A()"<< endl;}};// 仿函数删除器:用于释放malloc分配的内存template<classT>structFreeFunc{voidoperator()(T* ptr)const{ cout <<"FreeFunc: free memory at "<< ptr << endl;free(ptr);}};// 仿函数删除器:用于释放数组template<classT>structDeleteArrayFunc{voidoperator()(T* ptr)const{ cout <<"DeleteArrayFunc: delete[] memory at "<< ptr << endl;delete[] ptr;}};intmain(){// 使用FreeFunc删除器的shared_ptr shared_ptr<int>sp1((int*)malloc(sizeof(int)),FreeFunc<int>());*sp1 =100; cout <<"sp1: "<<*sp1 <<" at "<< sp1.get()<< endl;// 离开作用域时调用FreeFunc删除器// 使用DeleteArrayFunc删除器的shared_ptr shared_ptr<int>sp2(newint[5],DeleteArrayFunc<int>());for(int i =0; i <5;++i){ sp2.get()[i]= i;} cout <<"sp2 array:";for(int i =0; i <5;++i){ cout <<" "<< sp2.get()[i];} cout << endl;// 离开作用域时调用DeleteArrayFunc删除器// 使用lambda删除器管理A对象数组 shared_ptr<A>sp4(new A[3],[](A* p){ cout <<"Lambda: deleting array at "<< p << endl;delete[] p;}); cout <<"sp4 array of A objects created"<< endl;// 离开作用域时调用lambda删除器// 使用lambda删除器管理文件句柄 shared_ptr<FILE>sp5(fopen("test.txt","w"),[](FILE* p){if(p){ cout <<"Lambda: closing file"<< endl;fclose(p);}});if(sp5){fprintf(sp5.get(),"Hello, shared_ptr with deleter!\n"); cout <<"File written"<< endl;}// 离开作用域时调用lambda删除器关闭文件return0;}

对于所有的指针不一定是 new 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

Read more

无中生有——无监督学习的原理、算法与结构发现

无中生有——无监督学习的原理、算法与结构发现

“世界上绝大多数数据都没有标签。 真正的智能,不是在已知答案中选择,而是在混沌中发现秩序。” ——无监督学习的哲学 一、为什么需要无监督学习? 在前七章中,我们系统学习了监督学习(Supervised Learning)的核心范式:给定输入 x\mathbf{x}x 和对应标签 yyy,学习映射 f:x↦yf: \mathbf{x} \mapsto yf:x↦y。无论是线性回归、决策树,还是神经网络,都依赖于标注数据这一稀缺资源。 然而,现实世界的数据绝大多数是未标注的: * 用户浏览日志(只有行为,没有“好/坏”标签); * 医学影像(只有图像,没有诊断结论); * 社交网络(只有连接关系,没有群体划分); * 传感器时序(只有数值流,没有异常标记)

By Ne0inhk

力扣234.回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。 示例 1: 输入:head = [1,2,2,1] 输出:true 示例 2: 输入:head = [1,2] 输出:false 提示:链表中节点数目在范围[1, 105] 内0 <= Node.val <= 9 题目解读 回文链表:本质是单链表的一种特殊结构 —— 从链表头部到尾部遍历得到的节点值序列,和从尾部到头部遍历得到的序列完全一致,比如 "abba"、"12321",正读和反读都相同。

By Ne0inhk
Java 面试篇-MySQL 专题(如何定位慢查询、如何分析 SQL 语句、索引底层数据结构、什么是聚簇索引?什么是非聚簇索引?知道什么是回表查询?什么是覆盖索引?事务的特性、并发事务带来的问题?)

Java 面试篇-MySQL 专题(如何定位慢查询、如何分析 SQL 语句、索引底层数据结构、什么是聚簇索引?什么是非聚簇索引?知道什么是回表查询?什么是覆盖索引?事务的特性、并发事务带来的问题?)

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录         1.0 MySQL 中,如何定位慢查询?         2.0 发现了 SQL 语句执行很慢,如何分析呢?         3.0 什么是索引?         4.0 索引的底层数据结构了解过吗?         5.0 B 树与 B+ 树的区别是什么呢?         6.0 什么是聚簇索引?什么是非聚簇索引?         7.0 知道什么是回表查询吗?         8.0 知道什么是覆盖索引吗?         9.0 MySQL 超大分页怎么处理?         10.0 索引创建原则有哪些?         11.0 什么情况下索引失效?

By Ne0inhk
【算法通关指南:数据结构和算法篇 】链表相关算法题:1. 排队顺序,2.单向链表

【算法通关指南:数据结构和算法篇 】链表相关算法题:1. 排队顺序,2.单向链表

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、排队顺序 * 1.1题目 * 1.2算法原理 * 1.3代码 * 二、单向链表 * 2.1题目 * 2.2算法原理 * 2.3代码 * 总结与每日励志 前言 本专栏聚焦算法题实战,系统讲解算法模块:以《c++编程》,《数据结构和算法》《基础算法》《算法实战》 等几个板块以题带点,讲解思路与代码实现,帮助大家快速提升代码能力ps:本章节题目分两部分,比较基础笔者只附上代码供大家参考,其他的笔者会附上自己的思考和讲解,希望和大家一起努力见证自己的算法成长 一、排队顺序 1.1题目 链接:

By Ne0inhk