文章目录
为什么要有智慧指针?
当抛出异常时,程序会立即跳转到匹配的 catch 块,跳过 try 块中剩余的代码。因此,在 try 块中分配的资源(如动态内存、文件句柄等)可能不会被正确释放。为了避免这种情况,可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)和 RAII(Resource Acquisition Is Initialization)技术来确保资源在异常发生时得到正确清理。
智能指针 说白了 就是 指针管理
那他是怎么管理的呢?你可以先看看smart_ptr
RAII
资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
1、RAII管控资源释放
2、像指针一样
3、拷贝问题
smart_ptr
template<class T>
class SmartPtr
{
public:
// RAII
// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
// 1、RAII管控资源释放
// 2、像指针一样
// 3、拷贝问题
SmartPtr(T* ptr)
:_ptr(ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SmartPtr()
{
cout << "delete: " << _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
smart_ptr利用RAII技术在生命周期内管理了指针,但是如果发生智能指针的之间的拷贝就会导致析构两次的错误,而且指针之间要的就是浅拷贝因为普通指针就是浅拷贝。
int main()
{
SmartPtr<string> p1(new string("xxx"));
SmartPtr<string> p2(new string("111"));
p1 = p2;
//默认浅拷贝,析构两次p2
//
//p1的空间没有释放,内存泄露
//智慧指针模拟的是普通指针,这里的赋值要的就是浅拷贝
return 0;
}
下面我们来看看C++里提供的各种智能指针
auto_ptr
template <class T>
class auto_ptr
{
public:
//RAII
//像指针一样
auto_ptr(T* ptr)
:_ptr(ptr)
{}
// ap3(ap1)
// 管理权转移
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~auto_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
private:
T* _ptr;
};
管理权转移导致的指针为空问题
int main()
{
// C++98 一般实践中,很多公司明确规定不要用这个
bit::auto_ptr<A> ap1(new A(1));
bit::auto_ptr<A> ap2(new A(2));
// 管理权转移,拷贝时,会把被拷贝对象的资源管理权转移给拷贝对象
// 隐患:导致被拷贝对象悬空,访问就会出问题
bit::auto_ptr<A> ap3(ap1);
//ap1->_a++;// 崩溃
ap3->_a++;
return 0;
}
unique_ptr
简单粗暴,不让拷贝
template<class T>
class unique_ptr
{
public:
//RAII
//像指针一样
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
// ap3(ap1)
//防拷贝
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
可指针不让我拷贝也不行啊,所以就有了shared_ptr
shared_ptr
利用一个引用计数来共同管理某一个资源,那这个计数器如何设计?
用一个类内成员int _count行不行?
不行,每个智能指针对象的计数各自是各自的计数,个玩的个,我们期望指针都要访问一个独立的计数
用一个类内的静态Int变量行不行?
不行,类内静态变量属于这个类,也属于这个类的所有对象,这种情况就需要两个计数,那你静态只有一个,所以不行
我们期望一个资源伴随一个计数,于是这个计数就是new出来的int* count ,则一个指针指向资源,一个指针指向引用计数。
每多一个拷贝或赋值的智慧指针指向同一个资源,引用计数就加加,析构先减减计数,等计数为0就delete资源,和delete引用计数
那如何delete[ ] arr 呢?而且new 和 delete 一定要匹配,如何解决?
拷贝构造挺简单的,因为语法上限制了自己给自己拷贝构造,赋值也是一个硬茬,因为可以自己给自己赋值,sp1 = sp1;
又或者sp1 = sp5会发生啥?
sp1指向的资源的引用计数要减减,涉及到减减计数就要考虑是否计数是1,如果是就要析构资源,而后再让sp5指向的资源计数++;
如果是自己给自己赋值,那么就判断我资源指针和你的资源指针是否是同一个,如果是判断后直接返回。
如果不判断那么自己给自己赋值的话,上来就把资源释放,最后又赋值给自己,那么就是野指针了,资源指针和计数指针都是随机值了。
还会有这种间接的自己给自己赋值 ,直接判断是资源同一个指针后直接返回
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
, _pcount(new int(1))
{}
template<class D>
shared_ptr(T* ptr, D del)//shared_ptr(T* ptr = nullptr,D del)//传缺省参数时只能从右往左给缺省
:_ptr(ptr)
,_pcount(new int(1))
, _del(del)
{}
~shared_ptr()
{
if ((--(*_pcount)) == 0)
{
cout << "delete:" << _ptr << endl;
_del(_ptr);
delete _pcount;
}
}
//sp3(sp1)
shared_ptr(const shared_ptr<T>& up)
:_ptr(up._ptr)
,_pcount(up._pcount)
{
++(*_pcount);
}
shared_ptr<T>& operator=(const shared_ptr<T>& up)
{
//处理自己给自己赋值导致野指针
//1.s1 = s1
//2.s4 = s5 此种情况也是自己给自己赋值
if (_ptr == up._ptr)
return *this;
if (--(*_pcount) == 0)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
_ptr = up._ptr;
_pcount = up._pcount;
++(*_pcount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int use_count() const
{
return *_pcount;
}
T* get() const
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
//
function<void(T*)> _del = [](T* ptr) {delete ptr; };//删除器设置默认参数,默认智能指针用delete释放
};
直接用指针构造的shared_ptr的风险
int main()
{
int* a = new int();
ljh::shared_ptr<int> a1(a);
ljh::shared_ptr<int> a2(a);
return 0;
}
他们两个指针指向同一个资源,但是直接构造,会在构造函数里开辟两个引用计数,都是1,造成double free
shared_ptr循环引用
是什么?
我对象成员里面有一个智能指针指向你,你对象成员里面有一个智能指针指向我
现象
循环引用的现象也会造成double free,或者是给你看到资源没释放也就是内存泄漏
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a = 0)" << endl;
}
~A()
{
cout << this;
cout << " ~A()" << endl;
}
//private:
int _a;
};
struct Node
{
A _val;
/*ljh::weak_ptr<Node> _next;
ljh::weak_ptr<Node> _prev;*/
shared_ptr<Node> _next;
shared_ptr<Node> _prev;
};
int main()
{
shared_ptr<Node> sp1(new Node);
shared_ptr<Node> sp2(new Node);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->_next = sp2;
sp2->_prev = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
上面的代码在没有执行sp1->_next = sp2; sp2->_prev = sp1; 也就是让他们互相指向时,都没事,但当他们互相指向了以后就有问题了
为什么?
前置知识:大对象先析构,小成员后析构
B里面包含了A对象,但是是B先调用析构函数,然后A a再调用自己的析构,就是这么一个顺序
如果B里面有多个成员呢?
和成员变量在类中声明次序有关1 A 2 C,他们又是在栈上开辟的,所以后进先出 。
有了这个前置知识以后再看,两个对象出了main作用域,让其双方引用计数都减到1,然后就没有然后了,VS调试到最后其实就结束了。
sp1和sp2不再指向节点,只剩next和prev指向节点
但是如果此时某一个节点,比如说右边节点要析构,你调用了删除erase,根据前置知识,_prev就会析构,prev是shared_ptr,它调用析构就让左边节点引用计数减减到0,左边节点就析构了,又根据前置知识,则左边节点中成员next也要析构,就会又让右边节点析构,右边节点析构就又让成员prev析构,也就是又让左边节点析构。此时double free。
一句话就是你析构节点就double free,不析构那就内存泄漏。
又或者你用图上的逻辑来看,它会一直循环着你释放我我释放你。
怎么办?
weak_ptr就上场了,但注意 weak_ptr不是RAII智能指针,它专门用来解决shared_ptr循环引用问题
weak_ptr它解决的原理就是weak_ptr不增加引用计数,可以访问资源,不参与资源释放的管理
在memory库里 甚至 weak_ptr 都没有重载 —>运算符
既然不参与管理资源增加引用计数,那么左边右边的计数都还是1,出作用域就释放,那就没问题。
我们要解决循环引用,第一步先得认识这个场景会产生循环引用的问题,比如你里面有个share_ptr指向我,我里面有一个share_ptr指向你,甚至是我这个对象里成员结构中里保存有的智能指针指向你。然后我们才能利用weak_ptr解决它。
wake_ptr
wake_ptr 不支持用一个普通指针来构造成智能指针,他只支持 wake_ptr ptr默认构造 or 用一个shared_ptr类型智能指针来拷贝构造 或 赋值,因为它不参与资源释放的管理,只是单纯访问资源。
所以一开始如果不了解底层,就会想那既然wake_ptr这么牛,能解决解决shared_ptr的缺陷循环引用,那都用它不就完了?
因为我不参与管理资源啊,所以才能解决循环引用
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
库里的weak_ptr
定制删除器,解决智慧指针delete [ ]无法适配的问题
前置知识
lambda 是一个可调用对象,可调用对象分为四种,函数指针,仿函数,lambda,包装器
包装器可以把它当成函数指针
库里面是通过构造函数传入一个可调用对象来完成各种delete ; free; 甚至是文件关闭。
只有shared_ptr支持了定制化删除器,删除数组资源等,其他智能指针没支持。
库里面构造时给了两个模板参数,我们就用一个就可以了,因为本身shared_ptr类就有T模板可以给指针类型。
因为他是在构造函数里传入的定制删除器,并不是在类模板参数里面传入的,
我们要控制析构函数里的删除方式,用这个可调用对象删除,那析构函数怎么使用删除器的类型D呢?
你可以用一个成员变量_del保存你传入的删除器,但是因为外部没有D模板参数,D是构造函数的模板参数,不是类模板的模板参数,所以我不知道D的类型是什么,没法定义_del成员。
你可以给类模板增加参数,但是这和库实现不一致,所以我们采用第二种优雅的方法。
我们可以用包装器将可调用对象类型包装起来,也就是函数类型是确定的,函数参数都是shared_ptr的模板参数T*,
返回值都是void,因为不管你是new,还是malloc,或者文件,你释放时都是给函数传入T类型指针,函数返回值是空。
到这里解决了删除数组或者malloc等情况,但是默认指针又没法删除了,可以给_del默认初始化一个lambda对象,
让它默认就是delete ptr就可以了。
不要忘记赋值重载里也涉及计数减减和析构问题,所以它默认delete ptr 也要改为用删除器删除。