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

C++智能指针详解:告别内存泄漏,拥抱安全高效

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

引言:为什么引入智能指针?

1.C++手动释放内存的痛点:

  • 内存泄漏:忘记delete或异常导致未释放。
  • 野指针:访问已经释放的资源。
  • 重复释放:同一内存被释放多次。
  • 资源泄漏:不仅限于内存。
  • 代码复杂性与维护困难。

2.RAII(Resource Acquisition Is Initialization)原则:获取资源即初始化。

  • 核心思想:将资源的生命周期绑定到对象的生命周期。
  • 对象构造时获取资源,对象析构时自动释放资源。

3.智能指针作为RAII的实践者:

  • 智能指针是类模板,封装了原始指针,顾名思义就是比原始指针更智能。
  • 通过重载运算符(->,*)模拟原始指针的行为。
  • 核心价值:在析构函数中自动释放管理的资源,确保资源安全释放。
  • 引如现代C++标准(auto_ptr的教训与C++11的革新)。

一.智能指针的场景引入

在下面的程序中我们可以看到,new了以后,我们也delete了。但是new本身也有可能抛异常,如果是第一个那还好,array1未被成功分配,就无需释放资源,异常被捕获,无内存泄漏。但是如果第二个new失败,array1成功分配内存,array2抛异常,如果不做特殊处理,异常被main函数的catch捕获,array1的内存就泄漏了。在没有学智能指针之前,我们是按如下方式解决的,但是这让我们处理起来很麻烦。

double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";} else{return (double)a / (double)b;}
} 
void Func()
{int* array1 = new int[10];int* array2 = new int[10]; // 抛异常呢try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;} catch(...){cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;throw; // 异常重新抛出,捕获到什么抛出什么}cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2;
} 
int main()
{try{Func();}catch(const char* errmsg){cout << errmsg << endl;} catch(const exception & e){cout << e.what() << endl;} catch(...){cout << "未知异常" << endl;} return 0;
}

二.RAII和智能指针的设计思路

RAII是一种管理资源的类的设计思想,本质是一种利用对象生命周期来代管(做到共同管理)获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。

RAII在获取资源时把资源委托给一个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。

智能指针类除了满足了RAII的设计思路,还要方便了资源的访问,所以智能指针类还会像迭代器类一样,重载 operator*/operator->/operator[] 等运算符,方便访问资源。

下面我们就来看一下是怎么用智能智能解决上面new的问题的。

template<class T>
class SmartPtr
{public :// RAIISmartPtr(T* ptr): _ptr(ptr){}~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;} // 重载运算符,模拟指针的行为,方便访问资源T & operator*(){return *_ptr;} T* operator->(){return _ptr;} T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};
double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Divide by zero condition!";} else{return (double)a / (double)b;}
} void Func()
{// 这里使用RAII的智能指针类管理new出来的数组以后,程序简单多了//将资源的生命周期绑定到对象的生命周期//对象构造时获取资源,对象析构时自动释放资源SmartPtr<int> sp1 = new int[10];SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i;} int len, time;cin >> len >> time;cout << Divide(len, time) << endl;
} 
int main()
{try{Func();} catch(const char* errmsg){cout << errmsg << endl;} catch(const exception & e){cout << e.what() << endl;} catch(...){cout << "未知异常" << endl;} return 0;
}

通过前面对智能指针的简单了解,我们已经大概知道了智能指针就是帮助代管资源的,模拟指针的行为,访问修改资源。那么智能指针的行为应该就属于浅拷贝,浅拷贝有什么问题,导致多次析构资源,这个问题智能指针需要解决,接下来我们就开看看他是怎么解决这一问题的。

三.C++标准库智能指针的使用及原理

C++标准库中的智能指针都在<memory>这个头文件下面,我们包含<memory>就可以是使用了,智能指针有好几种,除了weak_ptr他们都符合RAII和像指针一样访问的行为。

原理上而言主要是解决智能指针拷贝时的思路不同。

auto_ptr

auto_ptr - C++ Reference是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会导致被拷贝对象悬空,访问报错的问题。

struct Date
{int _year;int _month;int _day;Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){}~Date(){cout << "~Date()" << endl;}
};int main()
{auto_ptr<Date> ap1(new Date);// 拷贝时,管理权限转移,被拷贝对象ap1悬空auto_ptr<Date> ap2(ap1);// 空指针访问,ap1对象已经悬空//ap1->_year++;return 0;
}

**视频演示**

auto_ptr屏幕录制

**原理**

拷贝时,资源管理权转移,ap2代管资源,被拷贝对象ap1悬空。

**模拟实现**

namespace xin
{template<class T>class auto_ptr{public:auto_ptr(T* ptr): _ptr(ptr){}auto_ptr(auto_ptr<T>& sp):_ptr(sp._ptr){sp._ptr = nullptr;//管理权转移}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (*this != ap){if (_ptr){//释放当前资源delete _ptr;}//将ap的资源转移给当前对象_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;};
}

unique_ptr

unique_ptr - C++ Reference是C++11设计出来的智能指针,他的名字翻译出来是唯一的指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。

int main()
{unique_ptr<Date> up1(new Date);// 不支持拷贝//unique_ptr<Date> up2(up1);// 支持移动,但是移动后up1也悬空,所以使用移动要谨慎//因为移动构造有被掠夺资源的风险,这里默认是你知道//你自己move的,就说明你知道有风险,所有才说他们本质是设计思路的不同unique_ptr<Date> up3(move(up1));return 0;
}

**视屏演示**

unique_ptr

**原理**

unique_ptr不支持拷贝,只支持移动。

**模拟实现**

template<class T>
class unique_ptr
{
public:explicit unique_ptr(T* ptr)//不支持隐士类型转化,避免原始指针隐士转化为智能指针:_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _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;}T& operator*(){return *_ptr;}T& operator->(){return _ptr;}private:T* _ptr;};

shared_ptr

shared_ptr - C++ Reference是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。

int main()
{shared_ptr<Date> sp1(new Date);// 支持拷贝shared_ptr<Date> sp2(sp1);shared_ptr<Date> sp3(sp2);cout << sp1.use_count() << endl;sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;// 支持移动,但是移动后sp1也悬空,所以使用移动要谨慎shared_ptr<Date> sp4(move(sp1));cout << sp4.use_count() << endl;return 0;
}

**视屏演示**

shared_ptr

**运行结果**

**原理**

他的特点是支持拷贝,也支持移动,底层是用引用计数的方式实现的。

引用计数就是统计有几个智能智能共同管理这块资源的,一个资源对应一个引用计数,不是sp1有一个自己的引用计数,sp2有一个自己的引用计数这种。看了图片大家就大概知道怎么理解引用计数了。这个跟操作系统里的文件系统里的硬链接,软链接计算引用计数那个挺像的。

智能指针析构时默认是用delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。但是因为new []经常使用,为了简洁一点,unique_ptr和shared_ptr都特化了一份[]的版本。

int main()
{//这样实现程序会崩溃/*unique_ptr<Date> up1(new Date[10]);shared_ptr<Date> sp1(new Date[10]);*/// 解决⽅案1// 因为new[]经常使⽤,所以unique_ptr和shared_ptr// 实现了⼀个特化版本,这个特化版本析构时用的delete[]unique_ptr<Date[]> up1(new Date[5]);shared_ptr<Date[]> sp1(new Date[5]);return 0;
}

智能指针支持在构造时给个删除器,所谓删除器本质就是一个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。

template<class T>
void DeleteArrayFunc(T* ptr)
{delete[] ptr;
}template<class T>
class DeleteArray
{public :void operator()(T* ptr){delete[] ptr;}
};
class Fclose
{public :void operator()(FILE* ptr){cout << "fclose:" << ptr << endl;fclose(ptr);}
};int main()
{// 解决方案2// 仿函数对象做删除器// unique_ptr和shared_ptr支持删除器的方式有所不同// unique_ptr是在类模板参数支持的,shared_ptr是构造函数参数支持的// unique_ptr<Date, DeleteArray<Date>> up2(new Date[5], DeleteArray<Date>());// 这里没有使用相同的方式还是挺坑的// 使用仿函数unique_ptr可以不在构造函数传递,因为仿函数类型构造的对象直接就可以调用// 但是下面的函数指针和lambda的类型不可以unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]);//可以不在构造函数传递shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>());//在构造函数传递// 函数指针做删除器unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// lambda表达式做删除器auto delArrOBJ = [](Date* ptr) {delete[] ptr; };//我们无法知道lambda的类型unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ);////但是这里要显示传类型,就用了decltype,其作用是查询表达式的类型shared_ptr<Date> sp4(new Date[5], delArrOBJ);// 实现其他资源管理的删除器shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl;fclose(ptr);});return 0;
}

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

shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是一个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。

int main()
{shared_ptr<Date> sp1(new Date(2024, 9, 11));shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);auto sp3 = make_shared<Date>(2024, 9, 11);shared_ptr<Date> sp4;//支持无参构造// if (sp1.operator bool())if (sp1)cout << "sp1 is not nullptr" << endl;if (!sp4)cout << "sp4 is nullptr" << endl;// 报错 因为它们的构造函数都不支持隐士类型转化//shared_ptr<Date> sp5 = new Date(2024, 9, 11);//unique_ptr<Date> sp6 = new Date(2024, 9, 11);return 0;
}

**模拟实现**

下面的代码中使用了atomic<int>而不是普通的int是为了实现线程安全的引用计数,后面会更详细介绍。注意这里是不能用static的,static成员是所有同一类型实例共享的,而不是每个资源独立的。

template<class T>
class shared_ptr
{
public:explicit shared_ptr(T* ptr = nullptr )//标准库里支持无参构造:_ptr(ptr),_pcount(new atomic<int>(1))//_pcount(new int(1)){}template<class D>shared_ptr(T* ptr ,D del):_ptr(ptr),_pcount(new int(1)),_del(del){}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount), _del(sp._del){++(*_pcount);}void release(){if (--(*_pcount)==0){//最后一个管理的对象,释放资源_del(_ptr);delete _pcount;_ptr = nullptr;_pcount = nullptr;}}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);_del = sp._del;}return *this;}~shared_ptr(){release();}T* get() const{return _ptr;}int use_count() const{return *_pcount;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;atomic<int>* _pcount; //原子操作//int* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };//包装器来包装删除器,默认使用lambda
};

四.循环引用和weak_ptr

shared_ptr导致的循环引用问题

shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。

struct ListNode
{int _data;std::shared_ptr<ListNode> _next;std::shared_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循环引⽤ -- 内存泄露shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

没有析构,内存泄漏。

如上图所述场景,n1和n2析构后,管理两个节点的引用计数减到1

1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。

2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。

3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。

4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。

• 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏。

weak_ptr版本:

struct ListNode
{int _data;// 这⾥改成weak_ptr,当n1->_next = n2;绑定shared_ptr时// 不增加n2的引用计数,不参与资源释放的管理,就不会形成循环引用了std::weak_ptr<ListNode> _next;std::weak_ptr<ListNode> _prev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{// 循环引⽤ -- 内存泄露std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);cout << n1.use_count() << endl;cout << n2.use_count() << endl;n1->_next = n2;n2->_prev = n1;cout << n1.use_count() << endl;cout << n2.use_count() << endl;return 0;
}

weak_ptr

weak_ptr - C++ Reference是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源。

weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_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;return 0;
}

**原理**

**模拟实现**

template<class T>
class weak_ptr
{
public:weak_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;}private:T* _ptr = nullptr;
};

我们这里实现的shared_ptr和weak_ptr都是以最简洁的方式实现的, 只能满足基本的功能,这里的weak_ptr lock等功能是无法实现的,想要实现就要/把shared_ptr和weak_ptr一起改了,把引用计数拿出来放到一个单独类型,shared_ptr 和weak_ptr都要存储指向这个类的对象才能实现,有兴趣可以去翻翻源代码。

五.shared_ptr的线程安全问题

还记得我们在上面shared_ptr的模拟实现部分使用的atomic。原子操作(atomic operation)指的是在多线程环境下不会被中断的操作。这里的atomic<int>是C++11引入的原子类型,用于保证对引用计数的增减操作是原子性的,从而使得shared_ptr的引用计数在多线程环境下是线程安全的,当然这个也可以用加锁来实现。这个和操作系统处理访问临界资源的原理高度相似。

简单来说,就是shared_ptr的引用计数本身是线程安全的,但是shared_ptr管理的对象本身并不是线程安全的。因为多个线程同时修改同一个shared_ptr管理的对象时,需要额外的同步措施。

创作不易,还请各位大佬支持~

http://www.dtcms.com/a/341333.html

相关文章:

  • 如何用Python打造PubMed API客户端:科研文献检索自动化实践
  • Nginx 的完整配置文件结构、配置语法以及模块详解
  • 鸿蒙语音播放模块设置为独立线程,保证播放流畅
  • 【clion】visual studio的sln转cmakelist并使用clion构建32位
  • HTML5 视频与音频完全指南:从基础的 <video> / <audio> 标签到现代 Web 媒体应用
  • Java 大视界 -- Java 大数据在智能医疗远程会诊数据管理与协同诊断优化中的应用(402)
  • Dify实现超长文档分割与大模型处理(流程简单,1.6版本亲测有效)
  • AI线索收集技术实战指南
  • 解决移植到别的地方的linux虚拟机系统不能的使用固定IP的桥接网络上网进行ssh连接
  • 单片机驱动继电器接口
  • JavaScript中的函数parseInt(string, radix)解析
  • 【java面试day16】mysql-覆盖索引
  • 三分钟速通SSH登录
  • 1.Shell脚本修炼手册之---为什么要学Shell编程?
  • MySQL高阶篇-数据库优化
  • [GraphRag]完全自动化处理任何文档为向量知识图谱:AbutionGraph如何让知识自动“活”起来?
  • ​​pytest+yaml+allure接口自动化测试框架
  • STM32F4 SDIO介绍及应用
  • DNS 深度解析:从域名导航到客户端访问全流程
  • AI 与加密监管思维的转变:从美联储谈到开发者视角
  • Cobbler 自动化部署服务介绍与部署指南
  • SpringBoot集成WebService
  • BioScientist Agent:用于药物重定位和作用机制解析的知识图谱增强型 LLM 生物医学代理技术报告
  • docker CI操作演示分享(第四期)
  • Fastdata极数:中国外卖行业趋势报告2025
  • 网络流量分析——基础知识
  • [特殊字符] 从文件到视频:日常数据修复全攻略
  • 奇怪的“bug”--数据库的“隐式转换”行为
  • Kafka如何保证消费确认与顺序消费?
  • Torch -- 卷积学习day4 -- 完整项目流程