目录
智能指针使用场景分析
如下图程序,我们new了以后,也delete,但是因为抛异常的原因,后面的delete没能执行,所以造成了内存泄漏
异常可能会引起内存泄漏类似的安全问题
在中间捕获异常后,不是为了拦截异常,只为了让资源释放,异常还是由最外层处理,而是重新抛出让最外层处理是为了捕获到异常可以记录日志
new也会抛异常,因为new底层调用operator new,operator new封装了malloc,封装malloc的核心目的是符合C++库里面处理错误的机制
#include <iostream>
using namespace std;
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Divide by zero condition!";
}
else
{
return (double)a / (double)b;
}
}
void Func()
{
// 这⾥可以看到如果发⽣除0错误抛出异常,另外下⾯的array和array2没有得到释放。
// 所以这⾥捕获异常后并不处理异常,异常还是交给外⾯处理,这⾥捕获了再重新抛出去。
// 但是如果array2new的时候抛异常呢,就还需要套⼀层捕获释放逻辑,这⾥更好解决⽅案
// 是智能指针,否则代码太戳了
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和智能指针的设计思路

仅看这里是很难明白他的设计意图的

换个角度解释:获取到一个资源之后,不要单独管理,建议把这个资源委托给一个对象去管理,这个资源在这个对象的生命周期内都是有效的,生命周期结束后自动释放该资源,就不需要手动释放资源,而是自动释放资源

迭代器底层可能是链表,数组,dequeue这样各种各样复杂的结构,那用一个类型进行封装,去重载operator*,operator->,达到拥有像指针一样的访问功能,这样就屏蔽底层的细节,这样就同样只是一个迭代器,是一种面向对象设计的一种巧妙的方式,也是一种封装的体现,智能指针也是如此,他是一个类,通过RAII体现了智能释放的那一部分,通过运算符重载来体现指针的那一部分,如下图

C++标准库智能指针的使用

智能指针真正的问题,是拷贝的问题

比如这里这个东西有智能指针管理了,但你不能排除你要传参传值,或者你自己要进行拷贝,比如这里的sp2拷贝sp1,但是拷贝就要出问题
这里有个指针管理这里的资源,我们拷贝的时候不写拷贝构造,也能支持拷贝,编译器会默认生成一个拷贝构造,默认生成的拷贝构造对于内置类型(任何类型的指针都是内置类型),对于内置类型只能完成浅拷贝或者值拷贝,所以这里运行会崩溃,同一份资源被两次释放
这里还不能单纯地使用深拷贝解决问题,因为比如容器(vector、list、string等),那些资源是属于这些容器的,这里一个容器拷贝给另外一个容器,就是期望说你把我的内容拷贝一份保存起来,拷贝跟我一样大的空间,一样大的值,智能指针跟这些容器不同,智能指针是代管资源,这些资源不属于它,但是那些资源是属于那些容器的
智能指针模拟的是指针的行为,让一个指针拷贝给另一个指针,期望还是浅拷贝,但是浅拷贝就会造成释放多次的情况,所以历史上就出现了多种方案

auto_ptr

支持自动释放,解决拷贝问题使用到了一个管理权转移的概念(浅拷贝),不是一个很漂亮的用法,会导致一个悬空的问题


有向前兼容的概念,用了它以后,取消的话,代码可能就编不过

unique_ptr
特点是不支持拷贝,只支持移动,如果你是一个右值(临时对象,匿名对象),把你的资源移动给别人,也就不存在悬空的问题,可以移动,但不支持拷贝
但是如果将一个左值move了以后,只要移动的话,右值的话就是将你的资源转走,就会悬空,所以要谨慎使用移动,因为他以为你是右值,普通的右值没问题,但是是move以后的右值就要小心了,一样存在悬空的问题

boost库中被称为scoped_ptr

shared_ptr

有些场景必然还是要拷贝的,但是要拷贝的话,还是浅拷贝,但是会析构多次,那在这里怎么办呢?这里就需要使用一个叫引用计数的方案来解决
引用计数:记录有多少个人指向这块资源,最后一个人析构后才释放资源

下图是演示:
支持拷贝

use_count就是可以获取其引用计数

这里有三次析构是因为前面还有auto_ptr和unique_ptr

也支持移动,这里将一个左值move了以后,只要移动的话,右值的话就是将你的资源转走,所以要谨慎使用移动,因为他以为你是右值,普通的右值没问题,但是是move以后的右值就要小心了,一样存在悬空的问题

weak_ptr
不是智能指针,也称为"弱指针",是shared_ptr的"小弟"

底层
定制删除器
智能指针析构时默认是进行delete释放资源的,就意味着如果不是new出来的资源,交给智能指针管理的话,析构时就会崩溃。智能指针支持在构造时给一个删除器(就是一个仿函数一般,可调用对象),如果将这个可调用对象传给其以后,就不会写死是delete,就会用这里删除器(仿函数)去释放

因为这里底层写死了是delete,所以这里的解决方案是给了一个删除器,支持在构造的时候给一个删除器,就是一个仿函数,在仿函数的operator\[\]中进行释放,它会用这个对象,把资源给这个对象来进行释放,这个删除器你想以什么方式释放你就定制就行,在构造的时候给 

演示一下删除器的概念


一个是要传类模板类型,一个是要传函数对象
unique_ptr不能在构造的时候给,shared_ptr只能在构造的时候给

shared_ptr直接将构造函数写成了一个类模板,所以这个类型可以让你自己去推,传什么仿函数他就自己推

这里的函数指针是模板,没有实例化成具体的函数,用不了

unique_ptr这里的void(*)(Date*),void返回值,Date*的参数,我有这里的一个函数指针类型,但是定义出来一个空指针,我不知道这个指针指向谁,只是定义出来这一个类型的函数指针,所以unique_ptr有些情况下构造函数也可以传参数,是指在使用函数指针的情况下,用函数指针你传类型,类型实例化出来的对象你使用不了,所以在构造函数的时候还得传过去,相当于用函数指针底层定义一个类型,那你在构造的时候还要将函数指针具体的值传过来

lambda底层有类型但是上层没有,底层编译器编译成仿函数了,那个类型名称是编译器生成的,我们在语法层拿不到那个类型,只能拿给auto自己推,或者把lambda对象给shared_ptr这个模板自己推
decltype是C++的一个东西,可以推导我们的一个对象的类型,这个类型可以拿来模板传参

总结:shared_ptr的定制删除器好用

当然,定制删除器有时候有点重,在这里写个反函数或者lambda,可以是可以,但是用起来有点繁琐,但是也有方式用起来比较简单:类模板这个位置实例化的时候不要对应给类型,而是给对应类型的\[\],这样他就会自动调用delete\[\],通过特化的方式实现的

\[\]的直接走特化版本即可
// 解决⽅案1
// 因为new[]经常使⽤,所以unique_ptr和shared_ptr
// 实现了⼀个特化版本,这个特化版本析构时⽤的delete[]
unique_ptr<Date[]> up1(new Date[5]);
shared_ptr<Date[]> sp1(new Date[5]);
更复杂的类型写lambda更舒服
make_shared

make_shared是一个可变参数模板,传几个参数都可以帮你推,构造一个资源,并返回shared_ptr,会让引用计数效率高一点
shared_ptr还可以借助make_shared(make_pair类似)去构造

补充
可以通过get来获取底层的原生指针


operator bool是将自定义类型转换成内置类型,在这里可以判断是否为空指针
现实中如下图!sp4这么写,就会隐式类型转换成bool
这里体现智能指针能直接做条件逻辑判断

智能指针也支持直接打印,因为重载了流插入
拷贝就用shared_ptr,不拷贝就用unique_ptr

上图写法unique_ptr和shared_ptr都是不支持的,因为他们的构造函数带了explicit,不让隐式类型转换,这里的意思是返回一个Date*,构造一个Date对象,再去拷贝构造,编译器这里优化成直接构造,目的是不想这种指针在有些场景被转换成智能指针,从而发生一些问题

智能指针的原理

对于shared_ptr中的引用计数的编写,不能各自均有一个引用计数,这样的话资源就永远也析构不了,给对应的count加上static也是不行的

所以以上方法是不行的
正确的方法是一个资源配一个引用计数

定义一个指针指向这个计数,而不是把计数存起来,而计数实际是在堆上

shared_ptrd的实现
默认是不需要写删除器的
#pragma once
namespace rh {
template<class T>
class shared_ptr {
public:
explicit shared_ptr(T* ptr == nullptr)
:_ptr(ptr),
_pcount(_pcount)
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
_pcount(sp._pcount)
{
++(*_pcount);
}
~shared_ptr()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
}
private:
T* _ptr;
int* _pcount;
};
}
shared_ptr和weak_ptr

循环引用的问题是shared_ptr的缺陷,特殊场景下就会出现循环引用的问题
这里导致了内存泄漏

为什么循环引用会导致内存泄漏?

对于中间步骤图,n1有两个智能指针管理者n1和prev,n2也有两个智能指针管理者n2和next,这样看着没有问题
但是最后一步,n1和n2析构了就出问题了,n1和n2出了作用域调用析构函数将引用计数都减为了1,然后现在两个节点分别由next和prev分别管理,这样就导致内存泄漏了

_next是左边节点的成员,左边节点delete以后,_next就释放了
_prev是右边节点的成员,右边节点delete之后,_prev就释放了
就形成了回旋镖似的循环引用,谁都会释放就形成了循环引用,导致内存泄漏
遇到这种场景,将_next和_prev的shared_ptr改成weak_ptr

weak不支持RAII,不是一个常规的指针
最大的特点是不增加引用计数
你把你的资源给我,但是我不加你的计数,不参与这个资源释放的管理,仅仅是指向这个资源,能找到这个资源,所以不增加引用计数,也就是这里的next和prev不参与

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返回false为没过期,返回true为过期了
下图这里就过期了

假设不提供operator*和operator->就是怕过期了你再去访问
但你就是想访问你指向的资源,你就可以调用lock这个函数,新增一个shared_ptr去管理这个资源,就可以通过这个shared_ptr去访问了
总结:weak_ptr有计数,但是不增加计数,不参与资源释放,指向的资源是有可能过期,可以用expired进行判断,不支持直接访问,原因就是害怕过期你还继续访问,里面只剩野指针,在没过期的情况下进行lock,哪怕其他的shared_ptr都释放了,还有一个lock新增的shared_ptr管理,计数++,是安全的,就相当于锁住这个资源不让他提前释放
weak_ptr不一定是有效的,因为它指向的shared_ptr可能被赋值,资源释放了,就失效了

shared_ptr的线程安全问题

比如说这里有个智能指针,将这个智能指针拷贝给另外一个智能指针,这两个智能指针同时在两个线程上,但是他们管理同一份资源,多个线程上的多个智能指针管理同一个资源的时候,进行拷贝和析构就会++计数,计数是共享的,同时去++的话,就会有所谓的线程安全问题
传统的方式就是加锁或者原子操作atomic<int>*就可以保证线程安全

智能指针本身是线程安全的,但是指向的资源不是线程安全的,指向的资源他管不了,因为在外面,所以要加锁
内存泄漏
资源不用了不释放就是内存泄漏


普通程序出现内存泄漏是问题不大