智能指针 smart pointer
一.智能指针及其发展史
什么是智能指针呢?
现阶段的智能指针(smart pointer)的一种通用实现技术,是使用引用计数(reference count)。智能指针类将一个
计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象的指针指向同一对象。
智能指针的原理就是管理资源的RALL机制(RALL机制便是通过利用对象的自动销毁,使得资源也具有了生命周
期,有了自动销毁,自动回收的功能)。RAII全称为Resource Acquisition Is Initialization,它是在一些面向对象语
言中的一种惯用法。资源分配即初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始
化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。RAII要求,资源的有效期与持有资源的对象的
生命期严格绑定,即由对象的构造函数完成资源的分配,同时由析构函数完成资源的释放。在这种要求下,只要
对象能正确地析构,就不会出现资源泄露问题。
从另一个角度看,智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对
象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动
态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
事实上,智能指针能够做的还有很多事情,例如处理线程安全,提供写时复制,确保协议,并且提供远程交互服
务。有能够为这些ESP (Extremely Smart Pointers)创建一般智能指针的方法,但是并没有涵盖进来。智能指针的大部
分使用是用于生存期控制,阶段控制。它们使用operator->和operator*来生成原始指针,这样智能指针看上去就像一
个普通指针。
智能指针的发展史:
第一阶段(C++98):
auto_ptr —— 自动指针
auto_ptr 的主要思想是 管理权转移
但其缺陷很大,当通过拷贝构造函数,通过操作符=赋值后,原来的那个智能指针对象就失效了。只有新的智能
指针对象可以有效使用了。被包装的指针指向的内存块就像是一份独享占用的财产,当通过复制构造,通过=赋
值传给别的智能指针对象时,原有的对象就失去了对那块内存区域的拥有权。
简单的说,也就是任何时候只能有一个智能指针对象指向那块内存区域,不能有两个对象同时指向那块内存区域。
第二阶段(C++03):
即在Boost中的智能指针:
scoped_ptr —— 守卫指针
scoped_ptr的主要思想是 防拷贝
share_ptr —— 共享指针
weak_ptr —— 用于解决share_ptr的循环引用缺陷问题
share_ptr的主要思想是 引用计数
boost库中提供了一种新型的智能指针shared_ptr,它解决了在多个指针间共享对象所有权的问题,同时也满足容
器对元素的要求,因而可以安全地放入容器中。
第三阶段(C++11):
unique_ptr
share_ptr
weak_ptr
unique_ptr还是运用了 防拷贝 的思想
unique_ptr是一种定义在<memory>中的智能指针。它持有对对象的独有权——两个unique_ptr不能指向一个对
象,不能进行复制操作只能进行移动操作。
二.智能指针的简单实现
(1)auto_ptr 的实现
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
_ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (_ptr != ap._ptr)
{
if (_ptr)
{
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
}
return *this;
}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
protected:
T* _ptr;
};
根据auto_ptr的原理我们能明白,如果按以下的写法:
AutoPtr<int>ap1(new int);
AutoPtr<int>ap2(ap1);
我们会发现同一块内存释放了两次,程序会奔溃。如图:
ap1 程序一样会崩溃,原因还是 ap1 被夺走管理权,所以这种编程思想及其不符合C++思想,所以它的设计思想就是有一定的缺陷。
因此,一般不推荐使用 auto_ptr 智能指针。
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T*_ptr)
:_ptr(ptr)
{}
~ScopedPtr()
{
delete _ptr;
}
T &operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
protected:
ScopedPtr(ScopedPtr<T>& s);
ScopedPtr<T> operator=(ScopedPtr<T>& s);
protected:
T*_ptr;
};
并且只声明不实现,这样就实现了 防拷贝 的原理。
protected:
ScopedPtr(ScopedPtr<T>& s);
ScopedPtr<T> operator=(ScopedPtr<T>& s);
template<class T>
class SharePtr
{
public:
SharePtr(T* ptr)
:_ptr(ptr)
, _refCount(new int(1))
{}
~SharePtr()
{
Release();
}
inline void Release()
{
if (--*_refCount == 0)
{
delete _ptr;
delete _refCount;
_ptr = NULL;
_refCount = NULL;
}
}
T &operator*()
{
return *_ptr;
}
T*operator->()
{
return _ptr;
}
SharePtr(const SharePtr<T>& sp)
:_ptr(sp._ptr)
, _refCount(sp._refCount)
{
++(*_refCount);
}
SharePtr<T>&operator=(const SharePtr<T>&sp)
{
if (_ptr != sp._ptr)
{
Release();
_ptr = sp._ptr;
_refCount = sp._refCount;
++(*_refCount);
}
return *this;
}
protected:
T* _ptr;
int* _refCount;
};
share_ptr 使用时应该注意,在实例化后delete时,SharePtr<int>sp1<new int> 与 SharePtr<string>sp1<newstring[20]>的不同。前者是正确的,而后者就会使程序奔溃,因为后者会多开4个字节的空间,导致释放发生错误。
如图所示,若使用delete,则会造成内存泄露。因此,要使用 share_array 中的 operator[ ],用delete[ ] 来释放。
share_ptr 因为它采用引用计数的思想,所有它是功能较为完善的,但是 share_ptr 还是有缺陷的,例如在解决 循环引用 问题上。
三.循环引用
struct Node
{
shared_ptr<Node> _prev;
shared_ptr<Node> _next;
~Node()
{
cout << "~Node():" << this << endl;
}
int data;
};
void FunTest()
{
shared_ptr<Node> cur(new Node);
shared_ptr<Node> next(new Node);
cur->_next = next;
next->_prev = cur;
cout << "cur.use_count:"<<cur.use_count() << endl;
cout << "next.use_count:"<<next.use_count() << endl;
}
int main()
{
FunTest();
return 0;
}
运行结果:
可以看到 shared_ptr 的使用使得一块空间有两个对象管理,即头个结点的_next域和下一个指针共同管理,或者又头一个指针和第二个结点的_ptrv域共同管理所以其_refCount=2,这就是我们所谓的 循环引用 问题。
如图所示,这里导致的问题就是内存泄漏,这段空间一直都没有释放,现在很明显引用计数在这里就不是很合适
了,但是shared_ptr除了这里不够完善,其他功能还是很不错的,所以这里补充一个week_ptr,来解决其循环引用问题,接下来我们看最后一个智能指针week_ptr。