当前位置: 首页 > 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 出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除


希望读者们多多三连支持

小编会继续更新

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

请添加图片描述

http://www.lqws.cn/news/127459.html

相关文章:

  • 20-项目部署(Docker)
  • haribote原型系统改进方向
  • 编程笔试练习(1)
  • 6.4本日总结
  • mac版excel如何制作时长版环形图
  • Delft3D软件介绍及建模原理和步骤;Delft3D数值模拟溶质运移模型建立;地表水环境影响评价报告编写思路
  • 破解HTTP无状态:基于Java的Session与Cookie协同工作指南
  • 华为云服务器 Java 项目部署 “版本穿越” 危机破解指南
  • 机器学习基础(四) 决策树
  • linux系统--iptables实战案例
  • 技术文档的降维打击:3大原则+5步结构+CSDN流量密码
  • Windows下运行Redis并设置为开机自启的服务
  • Git忽略规则.gitignore不生效解决
  • 信号与系统汇总
  • zabbix 6 监控 docker 容器
  • 零基础玩转Python生物信息学:数据分析与算法实现
  • 【算法深练】分组循环:“分”出条理,化繁为简
  • 施耐德特价型号伺服电机VIA0703D31A1022、常见故障
  • 【趣味Html】第11课:动态闪烁发光粒子五角星博客
  • 基于大模型的结节性甲状腺肿智能诊疗系统技术方案
  • 人脸识别技术应用备案主体与条件
  • SVM超详细原理总结
  • 五大主流大模型推理引擎深度解析:llama.cpp、vLLM、SGLang、DeepSpeed和Unsloth的终极选择指南
  • css-塞贝尔曲线
  • 【hive sql】窗口函数
  • LINUX64 FTP 1; rsync inotify.sh脚本说明
  • 乡村三维建模 | 江苏农田无人机建模案例
  • hadoop集群启动没有datanode解决
  • 【Web应用】若依框架:基础篇15 源码阅读-权限注解
  • DeepSwiftSeek 开源软件 |用于 DeepSeek LLM 模型的 Swift 客户端 |轻量级和高效的 DeepSeek 核心功能通信