【C++11】智能指针
🦄个人主页:修修修也
🎏所属专栏:C++
⚙️操作环境:Visual Studio 2022
目录
为什么需要智能指针
智能指针的使用及其实现原理
RAII
auto_ptr
简介
实现
unique_ptr
简介
实现
简介
实现
weak_ptr
简介
实现
结语
为什么需要智能指针
C++没有垃圾回收的机制,必须通过我们手动的去动态申请并释放资源。也就是说, 我们new出的资源, 必须在后面不使用之后将其delete掉, 否则就会造成内存泄漏。
我在【C++】动态内存管理中详细介绍过内存泄漏的危害, 并提供了几种防止内存泄漏的方式, 其中最重要的一点就是要严格遵守谁申请谁释放原则, 这样就可以避免大量的内存泄漏场景, 一般而言,只要内存遵行了先申请后释放的原则, 那么100%是不可能内存泄漏的, 但是当C++引入异常这一特性的时候, 一切都变得不一样了...
我们来看这段代码, 分析一下下面三种情况程序会出现什么问题:
int div() { int a, b; cin >> a >> b; if (b == 0) throw invalid_argument("除0错误"); return a / b; } void Func() { // 1、如果p1这里new 抛异常会如何? // 2、如果p2这里new 抛异常会如何? // 3、如果div调用这里又会抛异常会如何? 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抛异常,剩下的两种情况都会导致不同程度的内存泄漏:
那么是不是有异常的时候我们只能对着在执行流里乱窜的异常说:"太好了孩子们,是异常,我们没救了😅"。当然不是!!!乱世之中, 咱们的救世主悄然登场了:
智能指针的使用及其实现原理
没错,智能指针就是救我们与水火之中的救世主, 那他的原理是什么呢?我们先来了解RAII思想:
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
它是在对象构造时获取资源, 接着控制对资源的访问使之在对象的生命周期内始终保持有效, 最后在对象析构时释放资源。因此, 我们实际上是把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源
- 采用这种方式, 对象所需要的资源在其生命周期内始终保持有效
auto_ptr
简介
借助RAII思想, 我们的初代智能指针auto_ptr闪亮登场, 只是可惜这个初代目不太经打, 刚刚登场, 就已领了盒饭, 以至于后面被很多企业禁止使用, auto_ptr的主要败笔在于, 它在拷贝构造和赋值时使用了管理权转移的思想, 即把b赋值/拷贝构造给a时, 会直接把b的指针给a, 然后b自己置空。这样会导致b指针后续处于一个悬空状态, 后续如果继续不慎使用b则会造成空指针解引用的问题。而且在b生命周期结束后他会去析构一个空指针, 这完全就是一个非法的内存访问操作, 所以不可避免的会导致程序崩溃。如果b指针赋值后不置为空, 那么后续又会出现多重析构的问题。这导致了auto_ptr面世没多久就遭到了"封杀", 实在是可惜。但是智能指针并不因此挫折就销声匿迹了, 有此前车之鉴, 后续大佬们又在auto_ptr的基础上开发出了更加安全, 实用的智能指针。
实现
以下是auto_ptr的简单模拟实现代码:
namespace test { template<class T> class auto_ptr { public: auto_ptr(T* ptr = nullptr) :_ptr(ptr) {} //管理权转移,把ap.ptr的管理权转让给_ptr,然后ap.ptr自己置空 auto_ptr(auto_ptr<T>& other) : _ptr(other.ptr) { other._ptr = nullptr; } ~auto_ptr() { if (_ptr) delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } // 赋值运算符重载,转移所有权 auto_ptr& operator=(auto_ptr& other) { if (this != &other) //检查是否自己给自己赋值 { delete _ptr; //先析构(放弃)自己原有的指针管理权 _ptr = other.ptr; //再拿到新的指针管理权 other.ptr = nullptr; //被转移对象自己置空 } return *this; } private: T* _ptr; }; }
unique_ptr
简介
auto_ptr的惨痛教训还历历在目, 于是C++痛定思痛, 推出了修补版本的unique_ptr。unique_ptr的想法是, 既然auto_ptr的赋值和拷贝构造会导致安全问题, 那么我把这两个操作禁用了不就ok了, 于是我们的二代智能指针就这样登场了。
实现
unique_ptr在实现上和auto_ptr几乎一模一样, 但是对于拷贝构造函数和赋值运算符重载函数则是直接"封掉":
namespace test { template<class T> class unique_ptr { public: unique_ptr(T* ptr) :_ptr(ptr) {} ~unique_ptr() { if (_ptr) delete _ptr; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //禁止拷贝构造 unique_ptr(const unique_ptr& other) = delete; //禁止赋值 unique_ptr& operator=(const unique_ptr& other) = delete; private: T* _ptr; }; }
shared_ptr
简介
unique_ptr的改进为我们提供了相对安全的智能指针的方案, 但是由于他的实现思想的限制, 导致该智能指针在应用上有诸多限制, 例如不可以多个指针管理同一份资源, 也不可以赋值更改管理的资源。为了解决这一问题, 大佬们又出研发了一个史诗级的智能指针, 就是shared_ptr。
shared_ptr借助了引用计数的思想, 支持多个智能指针管理同一份资源, 允许拷贝构造和赋值, 是通过为一份资源维护一份引用计数来实现的, 该引用计数记录了当前同时管理这份资源的智能指针数, 如果其中一个智能指针超出了生命周期要销毁资源,那么会先判断它是否是当前唯一管理这份资源,即引用计数是否为1, 如果是则析构释放资源, 如果不是那么只会将引用计数-1, 不会真实的去销毁资源。相应的,遇到拷贝构造和赋值则会相应的给引用计数+1。图示如下:
shared_ptr可以说是智能指针的中流砥柱了, 但是即便如此, 它在某些场景中还是会出现一些无解的问题, 即在某些情况下会出现循环引用问题:
首先我们可能会遇到使用智能指针管理链表资源的场景:
然后我们需要将这两个链表结点连接起来:
看起来好像没一点问题, 但是当我们要释放这两个结点的时候问题就出现了:
实现
shared_ptr的实现比前面两个稍复杂一点, 引入引用计数后, 我们需要在构造,析构,拷贝构造和赋值函数中对引用计数做相应的处理, 但只要清楚了引用计数的原理,实现起来也非常简单:
namespace test { template<class T> class shared_ptr { public: shared_ptr(T* ptr) :_ptr(ptr) ,_pcount(new int(1)) {} //如果引用计数为1才释放资源,否则只减引用计数 ~shared_ptr() { if (_ptr) { if (*_pcount == 1) { delete _ptr; delete _pcount; } else { (* _pcount)--; } } } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //拷贝构造 shared_ptr(const shared_ptr& other) :_ptr(other._ptr) ,_pcount(other._pcount) { (*_pcount)++; } //赋值 shared_ptr& operator=(const shared_ptr& other) { //判断自己给自己赋值 if (_ptr == other._ptr) { return *this; } //处理赋值对象管理的原资源 if (*_pcount == 1) { delete _ptr; delete _pcount; } else { (*_pcount)--; } //把新资源给赋值对象,同时增加引用计数 _ptr = other._ptr; _pcount = other._pcount; ++(*_pcount); return *this; } T* get() { return _ptr; } private: T* _ptr; int* _pcount; //用动态资源来管理引用计数,不能用普通类型,因为会导致每个类各自有一个 //也不能用静态成员, 因为不能修改 }; }
weak_ptr
简介
weak_ptr是一种弱引用智能指针, 它是为配合
shared_ptr
而引入的,主要用于解决shared_ptr
可能出现的循环引用问题。他不是RAII智能指针, 他不增加引用计数, 可以访问资源, 不参与资源释放的管理。
实现
因为weak_ptr不参与资源的管理,所以实现的时候非常简单, 析构函数, 拷贝构造函数和赋值运算符重载都不需要实现, 系统默认生成的就可以使用, 但是除此之外, weak_ptr还要支持用shared_ptr来构造和赋值, 所以我们实现这两个函数:
namespace test { template<class T> class weak_ptr { public: weak_ptr(T* ptr) :_ptr(ptr) {} T& operator*() { return *_ptr; } T* operator->() { return _ptr; } //要支持用shared_ptr构造 weak_ptr(const shared_ptr<T>& other) :_ptr(other._ptr) {} //支持用shared_ptr赋值 weak_ptr& operator=(const shared_ptr<T>& other) { _ptr = other.get(); return *this; } private: T* _ptr; }; }
结语
希望这篇关于 C++智能指针 的博客能对大家有所帮助,欢迎大佬们留言或私信与我交流.
学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!
相关文章推荐
【C++11】左值引用、右值引用、移动语义和完美转发
【C++】STL标准模板库容器set
【C++】模拟实现二叉搜索(排序)树
【C++】模拟实现priority_queue(优先级队列)
【C++】模拟实现queue
【C++】模拟实现stack
【C++】模拟实现list
【C++】模拟实现vector
【C++】标准库类型vector
【C++】模拟实现string类
【C++】标准库类型string
【C++】构建第一个C++类:Date类
【C++】类的六大默认成员函数及其特性(万字详解)
【C++】什么是类与对象?
实际就是把动态开辟的资源交给智能指针来管理.