一、智能指针简介
智能指针是C++标准库中的一个重要概念,主要用于管理动态分配内存的对象。与传统指针不同,智能指针能够自动管理内存的分配和释放,从而减少内存泄漏和其他内存相关错误的风险。C++中主要有三种智能指针:std::unique_ptr、std::shared_ptr和std::weak_ptr。
二、为什么要用智能指针
像前面抛异常的捕获try、catch就非常容易造成内存泄漏。
前面的C++异常中聊过,当throw执行时,throw后面的语句将不再被执行。所以当这里抛出runtime_error异常后,导致delete per被跳过。造成了内存泄漏。虽然我们可以通过对代码进行优化,以防止出现这种情况。但是如果用智能指针来对资源进行管理就会非常的方便。
C++98提供了std::auto_ptr C++11提供了std::unique_ptr 、std::shared_ptr、std::weak_ptr
三、 RAII和智能指针
RAII是Resource Acquisition Is Initialization(资源获取即初始化)的缩写,他是⼀种管理资源的类的设计思想,本质是⼀种利用对象生命周期来管理获取到的动态资源,避免资源泄漏,这里的资源可以是内存、文件指针、网络连接、互斥锁等等。RAII在获取资源时把资源委托给⼀个对象,接着控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源,这样保障了资源的正常释放,避免资源泄漏问题。
auto_ptr
auto_ptr是C++98时设计出来的智能指针,他的特点是拷贝时把被拷贝对象的资源的管理权转移给拷贝对象,这是⼀个非常糟糕的设计,因为他会到被拷贝对象悬空,访问报错的问题,C++11设计出新的智能指针后,强烈建议不要使用auto_ptr。其他C++11出来之前很多公司也是明令禁止使用这个智能指针的。
auto_ptr的简单模拟实现
unique_ptr
unique_ptr是C++11设计出来的智能指针,他的名字翻译出来是唯一指针,他的特点的不支持拷贝,只支持移动。如果不需要拷贝的场景就非常建议使用他。不允许左值赋值操作,可以通过move使左值转化为右值。移动后,移动的对象的指针也悬空,要谨慎使用。
跟auto_ptr主要区别是,用unique_ptr那么程序员是知道移动对象是悬空的情况下使用。
unique_ptr的简单模拟实现
shared_ptr
shared_ptr是C++11设计出来的智能指针,他的名字翻译出来是共享指针,他的特点是支持拷贝,也支持移动。如果需要拷贝的场景就需要使用他了。底层是用引用计数的方式实现的。
shared_ptr当复制或则拷贝的时候,引用计数+1,当智能指针析构的时候引用计数-1。如果引用计数为0,那么这块内存就没有资源了就释放它。
shared_ptr的简单模拟实现
shared_ptr 除了支持用指向资源的指针构造,还支持 make_shared 用初始化资源对象的值直接构造。
shared_ptr 和 unique_ptr 都支持了operator bool的类型转换,如果智能指针对象是⼀个空对象没有管理资源,则返回false,否则返回true,意味着我们可以直接把智能指针对象给if判断是否为空。
shared_ptr 和 unique_ptr 都得构造函数都使用explicit 修饰,防止普通指针隐式类型转换成智能指针对象。
shared_ptr的循环引用与weak_ptr
weak_ptr是C++11设计出来的智能指针,他的名字翻译出来是弱指针,他完全不同于上面的智能指针,他不支持RAII,也就意味着不能用它直接管理资源,weak_ptr的产生本质是要解决shared_ptr的⼀个循环引用导致内存泄漏的问题
hared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。 _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。 _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。 • 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏 • 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题
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访问资源是安全的。
当weak_ptr的expired函数返回值为 0(在 C++ 中,0 通常代表false)时,这意味着weak_ptr所关联shared_ptr仍然有效,即对应的对象尚未被销毁。打印出来引用计数是2,weak_pt不增加计数但是不代表不指向计数。
weak_ptr所关联的share_ptr过期了
调用lock返回⼀个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是⼀个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
四、删除器
智能指针析构时默认是进行delete释放资源,这也就意味着如果不是new出来的资源,交给智能指针管理,析构时就会崩溃。智能指针支持在构造时给⼀个删除器,所谓删除器本质就是⼀个可调用对象,这个可调用对象中实现你想要的释放资源的方式,当构造智能指针时,给了定制的删除器,在智能指针析构时就会调用删除器去释放资源。
因为new[]经常使用,所以为了简洁⼀点,unique_ptr和shared_ptr都特化了⼀份[]的版本,管理new []的资源。
定制删除器还有lambda版本和仿函数版本
在shared_ptr下建议使用lambda版本
在unique_ptr下建议使用仿函数版本