RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。
当我们把管理一份资源的责任交给一个对象,就不需要自己显示是释放资源,使用这种方式可以让对象所需资源在其生命周期内都有效。就是在对象构造的时候获取资源,然后让对象需要的资源在它的生命周期内都有效,在对象析构的时候把资源释放。
举一个简单的例子:
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr)
:_ptr(ptr)
{}
~SmartPtr()
{
delete _ptr;
cout << "delete" << endl;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<int> p(new int(2));
return 0;
}
比如上面那个smartptr的例子,智能指针需要重载* 、以及重载->才可以像指针一样使用。
智能指针需要有RAII的特性以及重载operator*和opertaor->让其像指针一样。
在c++98中就提供了auto_ptr的智能指针,下面就演示auto_ptr的使用以及问题
先简单模拟实现一个auto_ptr来理解他的原理
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>* ptr)
:_ptr(ptr)
{
ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
总结一下:auto_ptr通过转移控制权来实现智能指针。这样就不能实现两人智能指针同时指向一个资源。
在c++11中更新了更靠谱的unique_ptr, 这个智能指针就简单粗暴的限制了拷贝,下面简单实现一份unique_ptr来理解它的原理。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& up) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;
private:
T* _ptr;
};
就是一个很简单的防拷贝。这样的智能指针也并不是很好用。
C++11又提供了跟靠谱并且支持拷贝的的shared_ptr
shared_ptr的原理就是,通过添加引用计数来实现对象直接资源的共享,比如每天放学最后走的那位同学要关灯锁门,shared_ptr就是由最后一个人指向资源的对象来释放资源。
shared_ptr的简单实现:
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)
,_pcount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp)
,_pcount(sp._pcount)
{
(*_pcount)++;
}
T& operator=(const shared_ptr<T>& sp)
{
if (_ptr != sp)
{
release();
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
int usr_count() const
{
return * _pcount;
}
T* get()
{
return _ptr;
}
void release()
{
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
}
~shared_ptr()
{
release();
}
private:
T* _ptr;
int* _pcount;
};
shared_ptr有一个重大缺陷就是循环引用。
举个例子:
struct ListNode
{
int val;
crin::shared_ptr<ListNode> next;
crin::shared_ptr<ListNode> prev;
~ListNode()
{
cout << "delete" << endl;
}
};
void test_shared_ptr()
{
crin::shared_ptr<ListNode> n1 = new ListNode;
crin::shared_ptr<ListNode> n2 = new ListNode;
n1->next = n2;
n2->prev = n1;
}
在listnode对象销毁时打印delete。当两个shared_ptr对象互相引用是就会造成循环引用。
当调用test_shared_ptr函数时,并没有打印delete。这就说名listnode对象并没有被销毁,这就导致了内存泄漏。
循环引用分析:
1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上 一个节点。
4. 也就是说_next析构了,node2就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev 属于node2成员,所以这就叫循环引用,谁也不会释放。
他们都在等指向的对象释放,而指向的对象也再等指向的对象释放。这样就陷入了死循环。所以C++又提供了weak_ptr。
weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr。
1. weak_ptr指针基本不单独使用,要和shared_ptr 类型指针搭配使用。
2.?weak_ptr并没有重载operator->和operator *操作符
解决循环引用的方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和 _prev不会增加n1和n2的引用计数。
改成weak_ptr就完事了。
struct ListNode
{
int val;
crin::weak_ptr<ListNode> next;
crin::weak_ptr<ListNode> prev;
~ListNode()
{
cout << "delete" << endl;
}
};
void test_shared_ptr()
{
crin::shared_ptr<ListNode> n1 = new ListNode;
crin::shared_ptr<ListNode> n2 = new ListNode;
n1->next = n2;
n2->prev = n1;
}