1.RAII思想

1.1智能指针样例




当Division抛异常后,跳到上层捕获异常。智能指针出了作用域调用析构函数,就把开在堆上的空间释放了,就不需要重新抛异常了。
总结一下智能指针的原理:
1.RAII特性。
2.重载operator*和operator->,具有像指针一样的行为。
2.失败的设计——auto_ptr
上面讲解的内容都不是什么问题,都用的是以前的知识就解决了。智能指针难的地方是它的拷贝。
我们知道指针的拷贝都是浅拷贝,只需要相同的值就可以共同管理同一块空间。如果一个智能指针拷贝给另一个智能指针,出了它们的作用域调用析构函数时,我们发现同一个指针delete了两次,程序直接崩溃!
为了解决这个问题,C++98的标准库中给出了auto_ptr。

在auto_ptr走拷贝构造的时候,会把sp1的资源转移给sp2。这样sp1就为空了,这样虽然解决了出作用域会析构两次的问题,但是sp1用不了了,它已经为空了,如果我们后面要用sp1的话,可能会涉及到空指针解引用的问题,程序还是崩溃。所以这个设计是失败的,被人骂了很久很久...很多公司明确规定禁止使用!
3.unique_ptr
这是C++11标准库中的一个智能指针。

它最大的特点就是不让拷贝构造和赋值重载。

如果我们把错误的语法屏蔽掉,看看运行效果:

当然,我们也可以获取到原生指针:

调用operator->:

unique_ptr的底层很简单,就是把拷贝和赋值禁掉,这样不支持拷贝也不支持赋值。
4.shared_ptr
在有些场景下,指针需要拷贝。unique_ptr不支持拷贝也不太行(功能不全),而shared_ptr是全能的智能指针,它支持拷贝!

4.1shared_ptr的底层设计
shared_ptr支持拷贝和赋值,那么必然有两个对象指向同一块内存空间,为了让它们在析构的时候只释放一次,shared_ptr增加了一个引用计数。让shared_ptr拷贝一次,引用计数就+1,每销毁一次,引用计数就-1,当引用计数减为0的时候,才会将该智能指针指向的那块内存空间释放!

use_count可以得到该智能指针的引用计数,我们发现引用计数刚开始是2,拷贝了p3后引用计数变成3,出了作用域销毁p3后,引用计数又变回2。
4.2make_shared构造

除了可以直接构造智能指针,还可以用make_shared构造智能指针。这两个构造的功能几乎一样,只是make_shared有一个小优势:内存碎片少一些。
5.shared_ptr的模拟实现
#pragma once
#include <atomic>
namespace bit {
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new std::atomic<int>(1))//保证线程安全
{}
shared_ptr(const shared_ptr<T>& ptr)
:_ptr(ptr._ptr)
, _pcount(ptr._pcount)
{
(*_pcount)++;
}
void release() {
if (--(*_pcount) == 0) {
delete _ptr;
delete _pcount;
}
}
//p1 = p3;
shared_ptr<T>& operator=(const shared_ptr<T>& ptr){
if (_ptr != ptr._ptr) {//如果资源相同,啥都不用干
//先释放p1
this->release();
//再转移资源
_ptr = ptr._ptr;
_pcount = ptr._pcount;
//再增加计数器
++(*_pcount);
}
return *this;
}
~shared_ptr() {
if (--(*_pcount) == 0) {
delete _ptr;
delete _pcount;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
int use_count() {
return *_pcount;
}
private:
T* _ptr;
//保证线程安全
std::atomic<int>* _pcount;
};
}
shared_ptr的模拟实现比较简单,需要注意的是:赋值重载还有要保证计数器是线程安全的。
6.shared_ptr的线程安全问题

如上的程序,在链表插入的时候,对于链表来说是公有资源,多个线程可以同时向同一张链表中插入数据,所以不是线程安全的,需要加锁。
另外需要注意的是:在for循环中有一条智能指针拷贝构造的语句。进入循环,拷贝一次,引用计数++;出循环,析构一次,引用计数--。我们发现,对于同种类型的智能指针,引用计数是公有资源,所以在设计智能指针时,我们一定要保证引用计数在++、--的过程中是线程安全的,否则在面临这种多线程场景的时候,引用计数就可能会出现问题!
在C++标准库中当然考虑到了引用计数线程安全的问题,解决思路类似上文模拟实现中的解决方法,当然也可以shared_ptr中多定义一把锁,在引用计数++、--的时候加锁也可以解决。
所以在C++标准库中,智能指针的拷贝构造、析构是线程安全的;底层引用计数的加减也是线程安全的;但通过底层指针访问指向空间的过程不是线程安全的,需要加锁!
7.shared_ptr的缺陷——循环引用
看程序,观察运行结果:

这种情况是没有问题的,在p1,p2生命周期结束后,会调用它们的析构函数:引用计数减为0,释放Node*的指针,回去调用~Node()。

这种情况也是没有问题的,p1拷贝了一次,引用计数变成2。在p2生命周期结束调用析构函数,在释放p2中的指针时,会先释放p2->_prev,这样p1的引用计数减为1,随后p1的生命周期结束,调用析构函数。

但是这种情况就有问题了!观察程序的运行结果,我们发现并没有调用Node的析构函数。
因为p1和p2都被拷贝了一次,而且是相互指向的,所以它们的析构是相互影响的。当p2生命周期结束调用析构函数时,引用计数只会从2减到1,并不会释放p2中的指针,那么该指针中的资源也不会释放。在p1生命周期结束时,引用计数依然从2减到1,其他的什么都不做。这样就内存泄漏了!
这里不太好讲解,大家可以自己动手敲一敲代码,调试调试,体会一下。
这个缺陷没有什么解决办法,在敲代码的时候要注意不能这么玩!
8.weak_ptr
wead_ptr用来辅助解决shared_ptr循环引用带来的问题,不支持RAII,不单独管理资源。

破局的方法:将Node中的shared_ptr换成weak_ptr就可以了!
原因:在赋值或拷贝时,wead_ptr只是指向资源,让我们能够访问到,但是不会增加shared_ptr的引用计数,且不参与资源释放的管理,但是weak_ptr可以访问到shared_ptr的引用计数。这时引用计数始终为1,生命周期结束,资源释放!
8.1expired
weak_ptr有一个比较重要的操作就是判断是否过期。
如果shared_ptr生命周期已经结束,但是weak_ptr还可以访问到已经释放的资源,这是有风险的。expired可以判断资源是否过期。
举例:

0表示没有过期(还可以使用),1表示过期了(不能使用了)。
9.内存泄漏


1万+

被折叠的 条评论
为什么被折叠?



