智能指针 ——
指针 + RAII
RAII
:资源分配即初始化。定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化。在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
智能指针的发展历史及设计思想和缺陷
早期C++98 auto_ptr ——管理权转移. 这种方式有缺陷
boost(非官方) scoped_ptr/scoped_array ——守卫指针,防拷贝,简单粗暴
shared_ptr/shared_array ——共享指针,引用计数,复杂. 缺陷:循环引用
weak_ptr ——弱指针,不能单独存在,辅助解决shared_ptr的循环引用问题
C++11 unique_ptr
shared_ptr
weak_ptr
在我们实际写代码中,不免遇到下面的这种情况,这是非常危险的,会
导致程序出现内存泄漏
。
int* p1 = new int(2);
.....
if (p1)
{
return; //执行流改变
}
......
delete p1;
所以我们就引入了智能指针。
一个简单的例子来说明一下智能指针要做的事情(资源的分配和释放):
auto_ptr的管理权转移(模拟实现)
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
{
printf("0x%p\n", _ptr);
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
但这还不够,在实际中,我们还要写它的拷贝构造和赋值运算符的重载
因为出现这样的代码时,会出错
void TestAutoPtr()
{
AutoPtr<int> ap1(new int(2));
AutoPtr<int> ap2(ap1); //浅拷贝 ,两个对象指向同一块空间,会对同一块空间释放两次
}
所以我们要自己写拷贝构造和赋值运算符重载,进行
管理权转移
。
//ap2(ap1)
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
//ap1 = ap2
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
//测试
void TestAutoPtr()
{
AutoPtr<int> ap1(new int(2));
AutoPtr<int> ap2(ap1);
AutoPtr<int> ap3(new int(2));
ap2 = ap3;
*ap3 = 30; //因为ap3已被置NULL,再对其解引用就会出错
}
所以,对于AutoPtr我们还是不要使用它。
scope_ptr的防拷贝(模拟实现)
这里的防拷贝指的是不让拷贝,这样就不会出现同一块空间释放两次的情况
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr)
:_ptr(ptr)
{}
~ScopedPtr()
{
if (_ptr)
{
printf("0x%p\n", _ptr);
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//防拷贝,拷贝构造和赋值运算符重载定义为私有,且只声明不实现
private:
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr<T>& operator=(const ScopedPtr<T>& sp);
private:
T* _ptr;
};
void TestScopedPtr()
{
ScopedPtr<int> sp1(new int(1));
//ScopedPtr<int> sp2(sp1);
ScopedPtr<int> sp3(new int(2));
//sp3 = sp1;
}
shared_ptr的引用计数(模拟实现)
template<class T>
class SharedPtr
{
public:
friend class weak_ptr<T>; //随后会用到
SharedPtr(T* ptr)
:_ptr(ptr)
, _refCount(new int(1))
{}
~SharedPtr()
{
if (--(*_refCount) == 0)
{
printf("0x%p\n", _ptr);
delete _ptr;
delete _refCount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//sp2(sp1)
SharedPtr(const ScopedPtr<T>& sp)
:_ptr(sp._ptr)
, _refCount(sp._refCount)
{
(*_refCount)++;
}
//sp2 = sp1
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (this != &sp._ptr)
{
if (--(*_refCount) == 0)
{
delete _ptr;
delete _refCount;
}
_ptr = sp._ptr;
_refCount = sp._refCount;
(*_refCount)++;
}
return *this;
}
private:
T* _ptr;
int* _refCount;
};
在运行如下的代码时就会出现
循环引用
的问题
struct ListNode
{
int _data;
SharedPtr<ListNode> _next;
SharedPtr<ListNode> _prev;
ListNode(int x)
:_data(x)
, _next(NULL)
, _prev(NULL)
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test()
{
SharedPtr<ListNode> cur(new ListNode(1));
SharedPtr<ListNode> next(new ListNode(1));
cur->_next = next;
next->_prev = cur;
}
我们可以看出并没有对cur和next所管理的空间进行释放。
分析一下:
出作用域时cur和next的生命周期到了,就会调析构函数。cur->_next = next;
next->_prev = cur;cur等next释放,next等cur释放,这样就造成了
循环引用,导致内存泄漏
的问题。
shared_ptr单独使用的时候会出现循环引用的问题,造成内存泄漏,所以标准库又从boost库当中引入了weak_ptr(弱指针)。
注意:shared_ptr不能单独使用
由于弱指针并不修改该对象的引用计数,这意味这弱指针它并不对对象的内存进行管理,在功能上类似普通的指针。然而一个比较大的区别是:弱指针能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
下面我们来
模拟实现weak_ptr
template<class T>
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{}
WeakPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
{}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
WeakPtr<T>& operator= (const SharedPtr<T>& sp)
{
_ptr = sp._ptr;
return *this;
}
private:
T* _ptr;
};
可以看到,WeakPtr必须从一个SharedPtr或者另一个WeakPtr转换而来,不能使用new对象进行构造。这也说明,进行该对象的内存管理的是那个强引用的SharedPtr。WeakPtr只是提供了对管理对象一个访问手段。
然后再测试:
struct ListNode
{
int _data;
WeakPtr<ListNode> _next;
WeakPtr<ListNode> _prev;
ListNode(int x)
:_data(x)
, _next(NULL)
, _prev(NULL)
{
cout << "ListNode()" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void Test()
{
WeakPtr<ListNode> cur(new ListNode(1));
WeakPtr<ListNode> next(new ListNode(1));
cur->_next = next;
next->_prev = cur;
}
这样就对两个结点的空间成功的释放了。
因为弱指针不更改引用计数,
类似普通指针,只要把循环引用的一方使用弱指针,即可解除循环引用。