c++最令人失望的是没有资源回收功能,资源的回收依赖于显示定义的回收资源的函数。为了实现资源的自动回收,c++引入了一种能够近似自动回收的方法就是智能指针。
智能指针是基于一种叫做 RAII 的思想,(Resource Acquisition Is Initialization) 将资源的回收在定义变量的时候就设置好。智能指针的作用其实就是将资源的管理分配给每一个对象,随着对象的消亡而调用析构函数然后将资源粉碎掉。
这样做的好处是:
1.不需要显示的释放资源
2.采用这种方式,对象所需的资源在其生命周期内始终有效。
智能指针不应该只是一个对象,它还应该能够干指针能干的事情,比如说 * 解引用
和 -> 箭头,如果有需要的话还必须有 = 号功能。也就是说要对这些功能进行函数重载。
template <class T>
class AutoPtr
{
private:
T* _Ptr;
public:
AutoPtr(T *ptr = nullptr) :_Ptr(ptr)
{
}
AutoPtr(const AutoPtr<T> &ap) :_Ptr(ap._Ptr)
{
ap._Ptr = nullptr;
}
AutoPtr<T> &operator = (const AutoPtr<T> &ap)
{
if (this != &ap)
{
delete this->_Ptr;
this->_Ptr = ap._Ptr;
ap._Ptr = nullptr;
}
return *this;
}
~AutoPtr()
{
delete _Ptr;
}
T& operator *()
{
if (_Ptr == nullptr)
{
throw ap;
}
return *_Ptr;
}
T& operator ->()
{
if (_Ptr == nullptr)
{
throw ap;
}
return _Ptr;
}
};
1.资源只能被一个指针管理
2.如果使用一个管理了资源的指针去初始化另一个指针的话。那么管理资源的权限将会被转移。
3.转移过后的指针将会被赋予nullptr。也就是被悬空了。
以上存在的问题,如果空指针被错误的解引用会造成程序崩溃。
为了使得多个职能指针共同使用同一块空间,在以上的基础上做了一点改变。设立一个owner的bool变量,只有为true的owner才能够释放资源。使用拷贝构造函数或者赋值运算符时,会造成管理权限的转移。
template <class T>
class Auto_ptr{
private:
mutable T * _ptr;
mutable bool owner;
public:
Auto_ptr(T *ptr = nullptr) :_ptr(ptr), owner(true)
{}
Auto_ptr(const Auto_ptr & ap) :_ptr(ap._ptr), owner(true)
{
if (this != &ap)
{
ap.owner = false;
}
}
Auto_ptr<T> & operator = (const Auto_ptr & ap)
{
if (this != &ap)
{
if (ap)
{
ap.owner = false;
this->owner = true;
}
}
}
T & operator *()
{
if (_ptr)
{
return *_ptr;
}
}
T & operator ->()
{
if (_ptr)
{
return this->_ptr;
}
}
~Auto_ptr()
{
if (_ptr)
{
if (owner == true)
{
delete _ptr;
}
}
}
};
以上auto_ptr 的主要思想是:
1.多个指针对应一个资源
2.只有一个指针拥有对资源的释放的权利
3.使用拷贝构造函数和赋值运算符会将释放资源的权利进行转移。
从效果上来看第二种auto_ptr似乎更好一些。但是在实际运用中第二种auto_ptr有个致命的缺点。
请看下个例子
int main()
{
Auto_ptr<int> ptr1(new int(1));
if (1)
{
Auto_ptr<int> ptr2(ptr1);
}
*ptr1 = 10;
return 0;
}
如果在一个作用域里面ptr1调用拷贝构造函数构造ptr2。在作用域的生命周期结束后被自动释放。ptr1变成了野指针
Unique_ptr是c++11里面新增的一个智能指针版本。它的主旨非常暴力,既然auto_ptr有指针悬空问题。那么就让它不能赋值和拷贝就解决了问题。
template <class T>
class Unique_ptr
{
private:
T * _ptr;
Unique_ptr(const Unique_ptr<T> &) = delete;
Unique_ptr<T>&operator = (Unique_ptr<T> &ap) = delete;
public:
Unique_ptr(T * ptr = nullptr) :_ptr(ptr)
{}
~Unique_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator * ()
{
if (this->_ptr)
{
return *_ptr;
}
}
T* operator ->()
{
if (this->_ptr)
{
return _ptr;
}
}
};
阻止赋值和拷贝的方法很简单,将拷贝构造函数和赋值运算符设置为私有的,或者直接加c++ 11 的关键词 = delete。
承接string的深浅拷贝的思想。为了解决多个指针管理一份资源的需求。
shared_ptr加入了引用计数的方法。
简单来说就是使用一个指针管理计数空间,假设计数空间里的变量为count,在delete的时候检查计数count = 0 的时候释放资源。
值得注意的是,在进行++count 和 --count 时必须保证原子操作。多线程时候可能会引起误操作。
#include
#include
#include
using namespace std;
template <class T>
class Shared_ptr
{
private:
T * _ptr;
int * count;
mutex *_pMutex;
void Release()
{
if (_ptr && SubCount() == 0)
{
delete _ptr;
delete count;
}
}
void AddCount()
{
_pMutex->lock();
++(*count);
_pMutex->unlock();
}
int SubCount()
{
_pMutex->lock();
--(*count);
_pMutex->unlock();
return *count;
}
public:
Shared_ptr(T *ptr = nullptr) :_ptr(ptr), count(new int(1)), _pMutex(new mutex)
{
if (ptr == nullptr)
{
*count = 0;
}
}
~Shared_ptr()
{
Release();
}
Shared_ptr<T>(const Shared_ptr<T> &ap) :
_ptr(ap._ptr),
count(ap.count),
_pMutex(ap._pMutex)
{
if (_ptr)
{
AddCount();
}
}
Shared_ptr<T>& operator = (const Shared_ptr<T> &ap)
{
if (this->_ptr)
{
if (this->_ptr != ap._ptr)
{
Release();
this->_ptr = ap._ptr;
this->count = ap.count;
this->_pMutex = ap._pMutex;
if (_ptr)
{
AddCount();
}
}
}
return *this;
}
T & operator *()
{
if (_ptr)
{
return *_ptr;
}
}
T * operator ->()
{
if (_ptr)
{
return ptr;
}
}
int UseCount()
{
return *count;
}
T* Get()
{
return _ptr;
}
};
int main()
{
Shared_ptr<int> sp1(new int(10));
Shared_ptr<int> sp2(sp1);
*sp2 = 10;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
Shared_ptr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
sp1 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
system("pause");
return 0;
}
智能指针对象引用计数是多个智能指针对象共享的,两个线程中国的职能指针同时使用一个引用计数,造成错乱。最终导致资源没有被正确释放。为了解决这种潜在的问题,需要加一把锁。
由于智能指针指向的资源存放在堆上面,所以如果两个线程同时访问会导致线程安全问题。
#include
#include
using namespace std;
class ListNode
{
public:
int data;
shared_ptr<ListNode> pre;
shared_ptr<ListNode> next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> sp1(new ListNode);
shared_ptr<ListNode> sp2(new ListNode);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->next = sp2;
sp2->pre = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
假设建立一个双向循环链表,根据 main()函数的操作,如下来进行说明。
【图1】
shared_ptr sp1是一个智能指针,它的数据成员 ptr 维护了一块链表节点,Ref 维护了一块引用计数。同理可得 sp2。
【图2】
pre 和 next 也是智能指针,next->ptr 和 next->Ref 是空指针,他们不维护资源和引用计数。同理可得 pre。
【图3】
sp1->next = sp2; 该语句使得 sp1->next->ptr 指向了 sp2的资源,并且使得sp2->Ref 所维护的引用计数 +1,此时 sp2 的 count 为 2。
同理可得 sp2->pre = sp1;
【图4】
main()函数生命周期快结束时,调用构造函数对资源释放。以sp1 为例,sp1 指向的引用计数将会由2变为1,由于引用计数不为0,所以不对空间进行释放。同理可得sp2。
【图5】
main()函数的生命周期结束,在堆上出现了内存泄漏。两个节点互指双方,互不释放。造成了循环引用。
#include
#include
using namespace std;
class ListNode
{
public:
int data;
weak_ptr<ListNode> pre;
weak_ptr<ListNode> next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
shared_ptr<ListNode> sp1(new ListNode);
shared_ptr<ListNode> sp2(new ListNode);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->next = sp2;
sp2->pre = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
return 0;
}
改变的位置是 pre 和 next 的类型改为weak_ptr。
通过查看运行中的代码,我们可以的到如下关系。
【图1】
shared_ptr 和 weak_ptr 都继承自同一个基类,这个基类定义了指向资源的指针和引用计数的空间
利用了多态的思想进行设计。
【图2】
这里的引用计数也是一个由基类发展的多种不同类型的引用计数。
**
这里需要注意的是,在基类当中引用计数是两种数据,一种是uses代表着shared_ptr的引用计数,另一种是 weak 代表着 weak_ptr 的引用计数。也就是说 shared_ptr 其实有两个引用变量 uses 和 weak。weak_ptr 不能够自己开辟资源,但是能够管理shared_ptr 中的 weak计数 **
基于上面的构成,重新顺理一遍循环引用问题。
【图1】
与之前不同的地方,加入了weak的计数变量。在初始化一个shared_ptr时
,uses 和 weak 会同时++。
【图2】
sp1->next = sp2;sp2->pre = sp1; 经过该语句后weak_ptr类型的sp1->next 和 sp2->pre 指向的weak都加了1。
【图3】
假设先释放sp2,那么sp2->Ref->uses 就要-1,此时sp2->Ref->uses为0,说明要释放sp2->ptr所指向的资源。那么要释放sp2->ptr所指向的资源,就必须释放,sp2->ptr->pre。此时sp2->ptr->pre->sp2->Ref->weak 就要-1。sp2->ptr->pre->ptr 就要与sp1指向的资源断开联系。
【图4】
sp2已被销毁。但是由于 sp2->Ref->uses 和 sp2->Ref->weak 不同时为0,所以它依旧存在。
【图5】
现在释放sp1。 要释放sp1 ,那么sp1->Ref->uses 就要 -1。此时因为uses 为零,所要要释放 sp1->ptr所指向的资源。为了释放资源,就要先释放 sp1->ptr->next 。 此时sp1->ptr->next->Ref->weak 就要-1,
sp1 遗留的引用计数空间就要被销毁了(sp1->Ref->uses == 0 && sp1->Ref->weak == 0)。 最后由于sp1 的销毁 引起了 sp1->Ref->weak 的 -1 操作,此时 sp1->Ref 所指向的引用计数空间也就被销毁了。
我无敌