当前位置:首页 » 《我的小黑屋》 » 正文

C++笔记---智能指针

19 人参与  2024年12月01日 14:02  分类 : 《我的小黑屋》  评论

点击全文阅读


1. 什么是智能指针

1.1 RALL设计思想

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是一种资源管理类的设计思想,广泛应用于C++等支持对象导向编程的语言中。它的核心思想是将资源的管理与对象的生命周期紧密绑定,通过在对象的构造函数中获取资源,并在析构函数中释放资源,以此来确保资源总是被正确管理,避免资源泄露等问题。RAII利用了C++中对象自动调用析构函数的特性,即使在发生异常的情况下,也能保证资源被适时释放

1.2 智能指针的概念

智能指针是C++标准库中的一种高级指针管理工具,它们通过自动管理动态分配的内存来帮助预防内存泄漏和其他资源管理错误。

简单来说,智能指针是包装了指针的类,当智能指针对象的生命周期结束时,其析构函数会对指针指向的动态资源进行释放。

也就是说,我们将动态申请的内存交给智能指针对象来管理,将其的生命周期与智能指针的生命周期进行绑定,以此实现对动态分配的内存的自动释放。

智能指针相当于是给普通的指针加上了一个自动释放资源的功能,在行为上模拟普通指针的行为,重载了各种指针相关的操作符(operator*、operator->、operator[]等),因此叫做智能指针。

1.3 智能指针的应用场景

在C++中,智能指针的引入是为了简化和自动化内存管理,特别是在处理动态分配内存时,智能指针能够帮助防止常见的内存错误,如内存泄漏、悬挂指针和双重删除等。

在C++11之前,程序员需要手动管理动态分配的内存,这要求他们在适当的时机使用new和delete操作符来分配和释放内存。

这种管理方式不仅容易出错,而且在遇到异常时尤其难以保证资源被正确清理。

下面程序中我们可以看到,假如异常被抛出,就会导致array1和array2未被正常释放。

我们要处理这种情况,可以考虑先将异常捕获并将资源释放之后重新抛出,但这样的做法可维护性差,每此申请动态资源都需要再catch块中新增释放操作。除此之外,假如程序的其他部分抛出了异常(例如new也可能抛出异常),catch块中的释放操作也不会被执行。

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

当引入智能指针之后,操作起来就要简单得多了:

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(){SmartPtr<int> array1(new int[10]);SmartPtr<int> array2(new int[10]);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;}


2. C++标准库中的智能指针

智能指针在设计时存在一个较大的难题:那就是拷贝时的行为如何设计?

假如我们直接将一个智能指针内部的指针拷贝给另一个智能指针,那么当两个智能指针的生命周期都结束时,同一个动态资源就会被释放两次。

针对这个问题,C++标准库中给出了几种不同的方案,这些智能指针都在<memory>这个头文件下面,我们包含<memory>就可以使用了。

其中weak_ptr比较特殊,它仅用于辅助shared_ptr处理循环引用的问题,我们在第3部分再解释。

2.1 std::auto_ptr

auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是一个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr

C++11出来之前很多公司也是明令禁止使用这个智能指针的。

个人感觉auto_ptr是最贴合设计初衷的名字,应该给下面行为最接近普通指针shared_ptr使用,但可惜好名字被狗用了。

实现示例:

namespace lbz{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 = NULL;}return*this;}~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}} // 像指针一样使用T & operator*(){return *_ptr;} T* operator->(){return _ptr;}private:T* _ptr;};}

2.2 std::unique_ptr

std::unique_ptr是一种独占所有权的智能指针,它确保在任何时刻只有一个unique_ptr实例可以拥有和管理所指向的对象。一旦unique_ptr实例超出其作用域或被显式重置,它所管理的对象将被自动释放。

unique_ptr不支持拷贝操作,但可以通过移动语义来转让所有权。

实现示例
namespace lbz{template<class T>class unique_ptr{public:explicit unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 像指针⼀样使用T & operator*(){return *_ptr;} T* operator->(){return _ptr;} // 禁止直接拷贝unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;unique_ptr(unique_ptr<T>&& sp):_ptr(sp._ptr){sp._ptr = nullptr;} unique_ptr<T>& operator=(unique_ptr<T>&& sp){delete _ptr;_ptr = sp._ptr;sp._ptr = nullptr;}private:T* _ptr;};}

2.3 std::shared_ptr

std::shared_ptr提供了共享所有权的智能指针,允许多个shared_ptr实例共享同一个对象的所有权。对象的生命周期由最后一个持有该对象的shared_ptr实例决定。当没有任何shared_ptr实例指向该对象时,对象会被自动释放。

shared_ptr是行为最贴近普通指针的智能指针,实践当中的使用最多,所以一般提到智能指针默认就是指shared_ptr。

shared_ptr的原理

shared_ptr内部使用引用计数来跟踪当前有多少个shared_ptr实例正在引用同一个对象。

为了实现这一点,我们引入了一个动态申请的int对象来进行引用计数(即下面代码中的_pcount)。

当直接使用指针进行构造时,申请一个int对象为其计数;当进行拷贝构造时,int对象++。

注意,这里不能使用静态成员变量来进行计数,因为静态成员变量统计的是shared_ptr的数量,而不是指向某一个资源的shared_ptr的数量。

删除器

智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。

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

注意:unique_ptr的删除器通过在实例化模板时显式给出类型进行传递,shared_ptr只需要在构造函数的参数列表中给出可调用对象即可(lambda表达式使用更加方便)。

因为new[]经常使用,所以为了使用方便,unique_ptr和shared_ptr都特化了一份[]的版本:

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

在释放资源时,会用delete[]来进行释放。 

其他注意事项

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

template <class T, class... Args> shared_ptr<T> make_shared(Args&&... args);

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

3. shared_ptr 和 unique_ptr 的构造函数都使用了explicit 修饰,以防止普通指针隐式类型转换成智能指针对象。

实现示例

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

namespace lbz{template<class T>class shared_ptr{public:shared_ptr(T* ptr, function<void(T*)> del = ([](T* ptr) {delete ptr; })):_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){delete _pcount;_del(_ptr);_pcount = nullptr;_ptr = nullptr;}}~shared_ptr(){release();}shared_ptr<T>& operator=(const shared_ptr<T>& sp){// 只剩一个,且自己给自己赋值时必须这样解决if (_ptr != sp._ptr){release();_ptr = sp._ptr;_pcount = sp._pcount;_del = sp._del;++(*_pcount);}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}T* get() const{return _ptr;}int use_count() const{return *_pcount;}private:T* _ptr;int* _pcount;function<void(T*)> _del;};}

3. 循环引用

假如我们在双向链表中使用shared_ptr来管理资源会发生什么?

#include<memory>using namespace std;struct ListNode{ListNode(int val):_val(val){}int _val;std::shared_ptr<ListNode> _prev = nullptr;std::shared_ptr<ListNode> _next = nullptr;};int main(){std::shared_ptr<ListNode> n1(new ListNode(10));std::shared_ptr<ListNode> n2(new ListNode(20));n1->_next = n2;n2->_prev = n1;return 0;}

n1结点不仅被n1所指向,也被n2结点的prev所指向,引用计数为2,n2同理。

当程序结束时,n1、n2的生命周期结束,对应结点的引用计数各自减一,但此时两个结点仍各有1的引用计数,不会被正常释放。

只有当n2被释放,n2->prev的生命周期结束,n1才会被释放;但n2要被释放就需要n1先被释放,使n1->next的生命周期结束。

这样就导致两个结点互相引用、相互依存,始终不会被释放。

为了解决这个问题,就引入了weak_ptr来辅助shared_ptr的使用,weak_ptr相比于shared_ptr,其不会增加被指向资源的引用计数,也不负责资源的释放,将prev和next替换为weak_ptr就可以避免循环引用。

std::weak_ptr

std::weak_ptr是一种辅助智能指针,它与shared_ptr一起使用,用于避免循环引用问题。

相比于shared_ptr,weak_ptr不增加对象的引用计数,因此它不会延长对象的生命周期。

weak_ptr不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。

weak_ptr也没有重载operator* 和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。

namespace lbz{    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;};} 

4. 智能指针使用建议

优先使用unique_ptr来管理单个对象的所有权,因为它通常比shared_ptr更轻量级且更不容易产生错误。使用shared_ptr来管理共享所有权的资源,尤其是在多个对象或函数需要访问同一个资源的情况下。使用weak_ptr来避免与shared_ptr相关的循环引用问题。避免在全局作用域或静态存储持续期间使用智能指针,因为这可能会导致不必要的复杂性和潜在的资源管理问题。在可能发生异常的代码路径中使用智能指针,以确保即使在异常发生时资源也能被正确释放。当向容器或算法传递所有权时,使用std::move来将unique_ptr转换为右值引用,以便容器或算法可以接管所有权。

5. shared_ptr的线程安全问题

shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。

shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。

下面的程序会崩溃或者A资源没释放,bit::shared_ptr引用计数从int*改成atomic<int>*就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。

struct AA{int _a1 = 0;int _a2 = 0;~AA(){cout << "~AA()" << endl;}};int main(){bit::shared_ptr<AA> p(new AA);const size_t n = 100000;mutex mtx;auto func = [&](){for (size_t i = 0; i < n; ++i){// 这里智能指针拷贝会++计数bit::shared_ptr<AA> copy(p);{unique_lock<mutex> lk(mtx);copy->_a1++;copy->_a2++;}}};thread t1(func);thread t2(func);t1.join();t2.join();cout << p->_a1 << endl;cout << p->_a2 << endl;cout << p.use_count() << endl;return 0;}

6. C++11和boost中智能指针的关系

Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。

在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。

• C++ 98 中产生了第一个智能指针auto_ptr;
• C++ boost给出了更实用的scoped_ptr / scoped_array和shared_ptr / shared_array和

  weak_ptr等;
• C++ TR1,引入了shared_ptr等,不过注意的是TR1并不是标准版;
• C++ 11,引入了unique_ptr和shared_ptr和weak_ptr。

需要注意的是unique_ptr对应boost的scoped_ptr。并且这些智能指针的实现原理是参考boost中的实现的。

7. 内存泄露

7.1 内存泄露的定义和危害

内存泄露是指程序在运行过程中动态分配的内存未能被正确释放或回收,导致这部分内存无法供其他程序或同一程序的其他部分使用。随着时间的推移,未被释放的内存会被不断累积,可能导致系统可用内存减少,进而使程序运行变慢、响应时间增加,甚至可能引发系统崩溃或资源耗尽。

7.2 如何检测内存泄露

检测内存泄露通常需要使用专门的工具,这些工具可以在程序运行时分析内存的分配和释放情况。常见的内存泄露检测工具包括Valgrind、AddressSanitizer、Visual Studio的内存诊断工具等。这些工具能够帮助开发人员识别不被释放的内存分配,并定位到代码中的具体位置。

7.3 如何避免内存泄露

避免内存泄露的关键在于遵循良好的编程习惯和系统设计原则:

严格管理内存分配和释放,确保每次动态内存分配后都有相应的释放操作。使用智能指针和内存池等技术来自动管理内存,减少手动内存管理的错误。进行定期的代码审查,以发现潜在的内存泄露风险点。编写单元测试,确保内存管理的正确性。对于不熟悉的库或框架,仔细阅读文档,了解其内存管理的细节。

点击全文阅读


本文链接:http://m.zhangshiyu.com/post/194945.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1