由于程序员在写代码时容易忘记将申请的空间进行释放,或者一些复杂情况下无法正确释放空间,那么就会导致程序奔溃,故需要设计智能指针来帮助我们进行资源的管理。
由于类创建对象会自动调用构造函数,类对象的销毁会调用析构函数,那么借助这个特点,我们创建一个类,在对象创建时获取资源,在对象销毁时释放资源,让资源的获取和销毁都由类来管理,也就是用对象的生命周期来控制资源。
这样的做法有两个好处:
1.不需要显式的释放资源
2.保证资源在整个对象的生命周期内都是有效的
而智能指针具有RAII的特性,智能指针这个类对象在创建时会获取资源,在对象销毁时会释放资源,并且智能指针具有指针的功能,解引用,->,那么在智能指针内部是对解引用和->运算符进行了重载。
c++98中提供了auto_ptr,其解决浅拷贝的问题是用转移资源管理权限的方式
即当进行拷贝构造时,将当前指针设为NULL,将新的指针指向管理的资源
namespace bit {
template<class T>
class auto_ptr {
public:
auto_ptr(T* ptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
: _ptr(sp._ptr) {
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap) {
// 检测是否为自己给自己赋值
if (this != &ap) {
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
}
其实auto_ptr中资源权限转移的方式就是将释放资源的权限在拷贝构造或者赋值时给了新的auto_ptr,但是这样存在着巨大的问题:
int main()
{
std::auto_ptr<int> sp1(new int);
std::auto_ptr<int> sp2(sp1);
}
在上面这样的操作下,将sp1管理的资源转移给了sp2,此时sp1内部的指针被置为NULL,如果我们再对sp1进行操作,就会发生错误。
针对auto_ptr的问题,C++11中提出了解决方案,就是不简单粗暴的防拷贝,直接拒绝进行拷贝构造和赋值,做法是将拷贝构造和赋值私有化,并且只声明不定义。无法进行拷贝的话,就不存在浅拷贝的问题了。
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr)
: _ptr(ptr)
{}
~unique_ptr() {
if (_ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
还有一种支持拷贝的智能指针,在解决浅拷贝时是通过引用计数的方式来实现的。
原理:
1.shared_ptr内部有一个计数器,记录该资源被几个对象共享
2.在对象被销毁时,将计数器–
3.如果此时的计数器为0,说明当前销毁的对象是最后一个管理该资源的对象,就需要释放该资源
4.如果计数器不为0,说明还有其他对象在使用该资源,就不能释放该资源。
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pRefCount(new int(1))
, _pmtx(new mutex)
{}
shared_ptr(const shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pRefCount(sp._pRefCount)
, _pmtx(sp._pmtx) {
AddRef();
}
void Release() {
_pmtx->lock();
bool flag = false;
if (--(*_pRefCount) == 0 && _ptr) {
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pRefCount;
flag = true;
}
_pmtx->unlock();
if (flag == true) {
delete _pmtx;
}
}
void AddRef() {
_pmtx->lock();
++(*_pRefCount);
_pmtx->unlock();
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp) {
//判断是否是自己给自己赋值
if (_ptr != sp._ptr) {
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pmtx = sp._pmtx;
AddRef();
}
return *this;
}
int use_count() {
return *_pRefCount;
}
~shared_ptr() {
Release();
}
// 像指针一样使用
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T* get() const {
return _ptr;
}
private:
T* _ptr;
int* _pRefCount;
mutex* _pmtx;
};
shared_ptr的线程安全问题分两个方面:
1.shared_ptr内部具有++,–这样的操作,其本身不是原子性的,所以需要我们对其进行加锁保护,以保证其为原子性,所以在引用计数的操作上是线程安全的
2.智能指针管理的对象在堆上存放,两个线程去访问该资源,是存在线程安全问题的,但这个问题是访问的人处理的,智能指针不能管,也管不了。
struct ListNode{
int a;
shared_ptr<ListNode> prev;
shared_ptr<ListNode> next;
};
这样一个类对象,如果用来创建两个节点,并且互相指向
int main()
{
shared_ptr<ListNode> node1(new ListNode); //a
shared_ptr<ListNode> node2(new ListNode); //b
node1->next = node2; //c
node2->prev = node1; //d
}
此时我们分析一下:
a语句执行完,shared_ptr管理的node1资源计数器为1;b语句执行完,shared_ptr管理的node2资源计数器也为1;
当c语句执行完之后,由于node1->next也管理了node2管理的资源,那么node2智能指针的计数器会++为2,同理d语句执行完node1智能指针计数器也会变为2,此时程序结束,node2智能指针需要调用析构函数进行资源释放,按照原理,发现内部计数器在–之后依然!=0,则不会对其管理的ListNode进行释放,同理,node2也不会释放自己管理的资源,这样申请出来的两个节点的资源就会泄漏。
分析之后我们发现,根本原因是在智能指针管理的资源内部存在着shared_ptr,这样导致在指向另一个资源时,计数器进行了++,从而使得最后释放资源时无法正确释放。
解决方案:
使用weak_ptr,将ListNode中的shared_ptr指针替换掉。 本质是当weak_ptr指针指向原来空间时,引用计数不再++
struct ListNode{
int a;
weak_ptr<ListNode> prev;
weak_ptr<ListNode> next;
};
而weak_ptr在node1->next = node2时,计数器不会++,node2->prev = node1时,node1的引用计数也不会++,这样在最终进行资源释放时,就能够正确的进行释放了。