在 c 和 c++ 中,通过访问指针对象存储的地址,可以实现对内存的直接操作,但在实际工程中,由于复杂情况下意料外的程序跳转,程序很可能出现内存泄漏。
int* p = new int(1);
//若干代码
if (p)
{
return
}
//若干代码
delete p;
因此我们引入「智能指针」
智能指针的历史
智能指针是 RAII(Resource Acquistion Is Initialization)(资源分配就初始化)思想的产物
可以理解为:智能指针 = 指针 + RAII。
实际上, RAII 对于指针即为将指针封装为一个类,通过构造函数和析构函数管理指针,防止上述情况的内存泄漏。
一个典型的例子如下
template <class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
作为一个智能指针,上述代码并不合格,至少还应实现赋值等操作。
auto_ptr
第一个被实现的智能指针 auto_ptr 于c++98被引入,实现了 RAII,通过「管理权转移|实现拷贝构造、赋值。
但 auto_ptr 并不被建议使用,其通过直接将堆上数据地址转移到新指针地址的方式实现拷贝构造、赋值,此时误访问原指针时很容易引起程序的崩溃。
参考以下代码:
template<class T>
class auto_ptr
{
public:
//已省略不必要的代码
auto_ptr(auto_ptr<T> &a):_p(a._p)
{
a._p = NULL;
}
auto_ptr<T>& operator= (auto_ptr<T> a)
{
_ptr = a._ptr;
a._ptr = NULL;
return *this;
}
private:
T* _p;
}
针对上述缺陷, Boost 库引入 scoped_ptr ,后来也被 c++ 标准库(名为 unique )纳入。
scoped_ptr
scoped_ptr 通过限制拷贝构造和赋值运算的方式规避 auto_ptr 的缺陷。
具体:不对函数进行定义,使用 private 防止类外实现。
参考代码
template<class T>
class auto_ptr
{
public:
//已省略不必要的代码
private:
auto_ptr(auto_ptr<T> &a);
auto_ptr<T>& operator= (auto_ptr<T> a);
T* _p;
}
scoped_ptr 终究是一个先天不全的智能指针。
目前的最终版本是 shared_ptr & weak_ptr。
shared_ptr
shared_ptr 是目前使用最广泛的智能指针,随 Boost 引入并在 C++11 被加入标准库
通过「引用计数」 shared_ptr 主要实现如下
template<class T>
class shared_ptr
{
public:
friend class weak_ptr<T>; //随后会用到
shared_ptr(T* ptr = NULL):
_ptr(ptr)
,_refCount(int new(1)){}
shared_ptr(const shared_ptr<T> &p):
_ptr(p._ptr),
_refCount(p._refCount)
{
(*_refCount)++;
}
~shared_ptr()
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
}
shared_ptr<T>& operator= (const shared_ptr<T> &p) //重载赋值
{
if (_ptr != p._ptr)
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
_ptr = p._ptr;
_refCount = p._refCount;
}
}
T& operator* (const shared_ptr<T> &p) //重载解引用
{
return *_ptr;
}
T* operator-> () //重载 ->
{
return _ptr;
}
private:
T* _ptr;
int* _refCount;
循环引用问题
如下代码运行时,程序会出现无限循环问题,代码如下
template<class T>
class shared_ptr
{
//略
}
typedef shared_ptr<Node> test_ptr;
struct Node //链表节点的定义
{
dataType data;
test_ptr prev;
test_ptr next;
}
int main(void)
{
test_ptr p1(new Node);
test_ptr p2(new Node);
p1 -> next = p2;
p2 -> prev = p1;
return 0;
}
如下图所示,在 main 函数结束时,两个智能指针生命周期结束时会依次析构,但在在两者交叉相互指向时,会陷入死循环中:
分析过程容易发现:
main函数执行到末尾时,计数器对 p1, p2 的计数都是2
p2 结束生命周期时,需要先析构 p2-> prev( p1 的空间,计数-1),再析构 p2 本身的空间,计数-1.
p1 结束生命周期时,需要先析构 p1-> next( p2 的空间,计数器将置0,需要 delete p2 )
但delete p2 需要先析构 p2-> prev 即 p1,p1 的析构又需要先析构 p1-> next 即delete p2
故而程序死循环。
循环引用解决方案 —> weak_ptr
template<class T>
class weak_ptr
{
public:
weak_ptr(T* p = NULL):
_ptr(p){}
weak_ptr(const shared_ptr<T> &p):
_ptr(p._ptr){}
weak_ptr<T>& operator= (const shared_ptr<T> &p)
{
_ptr = p._ptr;
return *this;
}
//.....
private:
T* _ptr;
}
此时,将循环引用问题中的节点定义中的智能指针从 shared_ptr 改为 weak_ptr 即可。
//...
//typedef shared_ptr<Node> test_ptr;
typedef weak_ptr<Node> test_ptr;
//...