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

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

文章目录

  • 1.为什么需要智能指针?
  • 2.智能指针原理
    • 2.1 RAll
    • 2.2 像指针一样使用
  • 3.C++11的智能指针
    • 3.1 auto_ptr
    • 3.2 unique_ptr
    • 3.3 shared_ptr
    • 3.4 weak_ptr
  • 4.删除器
  • 希望读者们多多三连支持
  • 小编会继续更新
  • 你们的鼓励就是我前进的动力!

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

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

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

如果 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<class T>
class SmartPtr 
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}private:T* _ptr;
};int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<string> sp2(new string("xxx"));return 0;
}

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

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

这种做法有两大好处:

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

2.2 像指针一样使用

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

template<class T>
class SmartPtr
{
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<class T>
class auto_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<class T>
class unique_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<class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(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;}int use_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 其实也会有疏漏,比如:引用循环

struct ListNode
{int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;
};
int main()
{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;return 0;
}

当执行 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 ),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏

class ListNode 
{
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>using namespace std;class A 
{
public:~A() { cout << "A::~A()" << endl; }
};// 仿函数删除器:用于释放malloc分配的内存
template<class T>
struct FreeFunc 
{void operator()(T* ptr) const {cout << "FreeFunc: free memory at " << ptr << endl;free(ptr);}
};// 仿函数删除器:用于释放数组
template<class T>
struct DeleteArrayFunc 
{void operator()(T* ptr) const {cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl;delete[] ptr;}
};int main() 
{// 使用FreeFunc删除器的shared_ptrshared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>());*sp1 = 100;cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;// 离开作用域时调用FreeFunc删除器// 使用DeleteArrayFunc删除器的shared_ptrshared_ptr<int> sp2(new int[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删除器关闭文件return 0;
}

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


希望读者们多多三连支持

小编会继续更新

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

请添加图片描述

相关文章:

  • 【和春笋一起学C++】(十七)C++函数新特性——内联函数和引用变量
  • 在java 项目 springboot3.3 中 调用第三方接口(乙方),如何做到幂等操作(调用方为甲方,被调用方为乙方)? 以及啥是幂等操作?
  • 本地日记本,用于记录日常。
  • ④Pybullet之Informed RRT*算法介绍及示例
  • 四元数:从理论基础到实际应用的深度探索
  • .net jwt实现
  • 在Mathematica中实现Newton-Raphson迭代的收敛时间算法
  • 区块链架构深度解析:从 Genesis Block 到 Layer 2
  • Elasticsearch中的地理空间(Geo)数据类型介绍
  • 使用Virtual Serial Port Driver+com2tcp(tcp2com)进行两台电脑的串口通讯
  • 【运维实战】Rsync将一台主Web服务器上的文件和目录同步到另一台备份服务器!
  • ES海量数据更新及导入导出备份
  • 你工作中涉及的安全方面的测试有哪些怎么回答
  • 第6篇:中间件 SQL 重写与语义分析引擎实现原理
  • 瀚文(HelloWord)智能键盘项目深度剖析:从0到1的全流程解读
  • Ubuntu24.04 交叉编译 aarch64 ffmpeg
  • 旅游微信小程序制作指南
  • 高并发区块链系统实战:从架构设计到性能优化
  • 华为VanillaNet遇上BiFPN:YOLOv8的性能突破之旅
  • `<CLS>` 向量是 `logits` 计算的“原材料”,`logits` 是基于 `<CLS>` 向量的下游预测结果
  • 网站建设职能/手机seo快速排名
  • wordpress单位内网做网站/优化系统
  • 做视频网站挣钱吗/有什么引流客源的软件
  • 如何编辑做网站/nba总得分排行榜最新
  • nuxt做多页面网站/免费精准客源
  • 平凉建设局网站/服务器租用