目录
智能指针的使用及原理
RAII
std::auto_ptr
std::unique_ptr
正如我们在异常那篇博客里写到的,
void Func()
{int* p1 = new int[10];int* p2 = new int[10];//如果p2开辟空间失败,就抛异常,p1内存泄漏...}
我们在Func里刚开始new了一块空间,如果这块空间没有开辟成功,也不影响,直接抛异常到main函数。但是,如果在Func里刚开始开辟了两块空间p1、p2,如果p1开辟成功而p2开辟失败,这样p2抛异常就直接到main函数,p1开好的空间相当于内存泄漏了。
为了解决这样的问题,需要在p1下面再加一层try catch,但是如果我开辟了很多块空间,那就需要加多组try catch,其实,这样的问题可以用智能指针来解决。
智能指针的使用及原理
RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
1.不需要显式地释放资源。
2.采用这种方式,对象所需的资源在其生命期内始终保持有效。
我们使用RAII思想去实现一个智能指针:
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){delete[] _ptr;cout << "delete:" << _ptr << endl;}T* Get(){return _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T& operator[](size_t i){return _ptr[i];}
private:T* _ptr;
};int main()
{SmartPtr<int> sp1(new int[10]);return 0;
}
但是,当我们用sp1去拷贝构造sp2时,就会出现问题,因为这里是浅拷贝,sp1和sp2指向同一块空间,当程序结束时,会对同一块空间调用2次析构函数,导致程序崩溃。那么我们当用sp1去拷贝构造sp2,可以深拷贝吗?当然不行!我们的智能指针模拟的是指针的行为,本身就是应该浅拷贝,sp1和sp2应该指向同一块空间啊,那如何解决智能指针拷贝的问题呢?
std::auto_ptr
在C++98中,有auto_ptr,但是这是一个失败的设计,它的思想是管理权转移,
当拷贝之后,如果想再对sp1做一些动作,如赋值,这会导致程序崩溃。
由于这是一个失败的设计,因此很多公司明确禁止使用!
std::unique_ptr
为了解决auto_ptr的问题,unique_ptr的处理方式其实也很简单:
既然auto_ptr那样拷贝会导致悬空,那我直接禁止拷贝构造,所以,当需要用智能指针时,尽量用unique_ptr,没有风险。
但是,不支持拷贝也不能应对所有场景,在有些场景下,还是想拷贝智能指针,所以,又提出了shared_ptr。
关于unique_ptr我们可以参考网站unique_ptr
std::shared_ptr
shared_ptr支持拷贝,它引入了引用计数,之所以叫shared_ptr,是因为可以有多个指针指向同一个对象,
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减1。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
我们可以使用use_count函数得到某份资源的引用计数:
关于shared_ptr其他功能请参考网站shared_ptr
我们除了直接构造一个shared_ptr,还可以使用make_shared,这其实有点类似make_pair,这样的好处是能够减少内存碎片。
在面试时,经常需要手撕一个shared_ptr,那么就来看一下如何实现它:
每份资源应该对应一个引用计数,每个shared_ptr对象都会包含两个指针,一个指针指向这份资源,另一个指针指向这份资源的计数。
namespace ghs
{template <class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}//sp2(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){(*_pcount)++;}int use_count(){return *_pcount;}//sp3 = sp1shared_ptr<T>& operator=(const shared_ptr<T>& sp){//if (this != &sp) // 不建议这样写,如果sp1和sp2指向同一块空间,//这时sp1=sp2,会进去,但实际上没必要if(_ptr != sp._ptr)//这样比肯定没问题,不是指向同一块空间,才进去{this->release();_ptr = sp._ptr;_pcount = sp._pcount;(*_pcount)++;}return *this;}void release(){if (--(*_pcount) == 0){//最后一个管理的对象,释放资源delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){release();}private:T* _ptr;int* _pcount;};
}
从上面的shared_ptr模拟实现中,我们看到每份资源都要有一个引用计数,是在堆上开的一小块内存,当在堆上开大量小块内存时,会出现内存碎片的问题,所以库里的shared_ptr可以传内存池解决内存碎片的问题。
所以,就像上面所说,使用make_shared后,可以将资源和引用计数的空间开在一起。给make_shared一个A(模版参数),在把A的初始化给我,用这个初始化列表构造这个A,然后在A的头上多开4个字节放引用计数,也就是把资源和计数绑到一起,从而减少碎片化。
std::shared_ptr的线程安全问题
我们先来看这样一段代码:
void func(ghs::shared_ptr<std::list<int>> sp, int n)
{for (int i = 0; i < n; i++){sp->push_back(i);}
}int main()
{ghs::shared_ptr<std::list<int>> sp1(new std::list<int>);std::thread t1(func, sp1, 10000);std::thread t2(func, sp1, 20000);t1.join();t2.join();std::cout << sp1->size();return 0;
}
创建一个智能指针指向一个链表,并创建两个线程都调用func函数,func函数负责向链表里插入数据,当我们运行这个程序时,发现程序崩溃了,
所以,多线程在访问sp所指向的链表这份公共资源的时候不是线程安全的,因此,我们可以在访问链表时加锁:
这样就不会崩溃了,每次都能正常运行。
我们再来看这样一个问题:
在func里对sp进行拷贝,这时运行结果就会出现异常,这是什么原因呢?明明我们在插入数据时进行了加锁,但是运行结果还会异常,那这可能就说明上面多调的拷贝构造不是线程安全的!实际上,我们来观察一下各个位置的引用计数,发现引用计数这里已经出现了异常,
这是因为,线程1和线程2都在调用自己的拷贝构造,在各自调用拷贝构造时,会让引用计数+1,声明周期到了引用计数-1,但是多线程对这个引用计数+-不是线程安全的,两个线程拷贝智能指针要++计数,智能指针析构要--计数,因此,要保证引用计数的线程安全。
那么为了保证引用计数是线程安全的,又两种方法:
1)给每个资源加一个锁指针,在构造函数中初始化这把锁,在引用计数++--时调用这把锁。
2)直接把引用计数设为atomic(包含atomic头文件)
因此,我们需要知道,智能指针(库中的)对象本身拷贝是线程安全的,底层引用计数加减是线程安全的,但是智能指针指向的资源(如链表)访问不是线程安全的!
std::shared_ptr循环引用
我们来看这样一组对比:
下面把注释放开:
发现并没有调用到Node的析构函数,也就是造成了内存泄漏。我们来看这样几种情况:
1)两个节点没有相互指向。
2)前一个节点的_next指向后一个节点。
p2后定义先析构,引用计数由2变成1,然后p1再析构,引用计数由1变成2,p1指向Node中的_next在p1析构后就释放了,这就导致p2的引用计数-1,变成0,就把p2释放了,然后回头把p1也释放了。
3)前一个节点的_next指向后一个节点,后一个节点的_prev指向前一个节点。
p2后定义先析构,p2先析构,其引用计数-1,变成1,p1再析构,p1的引用计数也减到1。然后,右边节点在在左边节点析构后,_next指向的后一个节点引用计数才减到0;左边节点在右边节点析构后,_prev指向的前一个节点的引用计数才减到0,相当于前后两个节点互相制约着,谁都不撒手,这就是循环引用!循环引用的释放逻辑是一个死循环!
为了解决循环引用这样的问题,引入了weak_ptr,
weak_ptr没有采用RAII的思想,
C++中要求,在出现循环引用时,需要把_next和_prev改成weak_ptr,改成weak_ptr就可以解决循环引用问题的原因是赋值或拷贝时,只指向资源,但不增加shared_ptr的引用计数,但是weak_ptr可以访问到shared_ptr的引用计数,如下图:
定制删除器
我们来看这样一个例子:
但是如果把<>参数换成A[]就没问题,
但如果我们让shared_ptr指向一个文件,那就会崩溃了:
所以我们需要搞一个定制删除器,这个定制删除器通过构造函数的参数传导,
如果不传D del这个参数,就用delete去删,如果传了del,就用del去删,