为什么会有智能指针?
在我们现实生活中有这样一个使用场景,每次我们new或者malloc了一块空间,完了必须需要我们手动delete或者free释放这块空间,否则会出现内存泄漏。但是,实际应用中程序未必都会按照顺序执行到释放空间那一步,有时候会在中间部分抛出异常使执行流发生跳转或者发生其他状况,总之代码未按照我们期望的那样去执行,就有可能导致内存泄漏。这时候我们就引入智能指针(自动化管理指针所指向的内存资源的释放),主要是利用RAII机制,RAII它是在面向对象语言中的一种惯用法。资源分配以及初始化,定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
智能指针的发展史:
第一个阶段(C++98):
auto_ptr:自动指针,它的主要思想是管理权转移,但其存在很大的缺陷,当要进行拷贝构造或者赋值相关操作时,原来的智能指针就失去意义,此时只有新的智能指针可以使用。简单点说,就是任何时候只能有一个智能指针指向那块空间,这样在使用上造成了很大的不便。
第二个阶段(C++03):
即第三方库Boost库中的智能指针:
scoped_ptr:守卫指针,它的主要思想就是防拷贝。
shared_ptr:共享指针,顾名思义就是共享同一块空间,它的主要思想是引入引用计数,但存在循环引用的缺陷。
weak_ptr:弱指针,它主要用来配合共享指针解决共享指针的循环引用的缺陷。
第三阶段(C++11):
unique_ptr:如同boost库中的scoped_ptr,主要思想就是防拷贝。
shared_ptr:它的主要思想是引入引用计数,但存在循环引用的缺陷。
weak_ptr:弱指针,它主要用来配合共享指针解决共享指针的循环引用的缺陷。
auto_ptr/scoped_ptr/shared_pr/weak_ptr的设计思想、缺陷?
<1>auto_ptr:
#include<iostream>
#include<string>
using namespace std;
template <class T>
class AutoPtr
{
public:
//构造函数
AutoPtr(T* ptr = NULL)
:_ptr(ptr)
{}
//拷贝构造
AutoPtr(AutoPtr<T>& ap)
{
//管理权转移
_ptr = ap._ptr;
ap._ptr = NULL;
}
//赋值运算符重载
AutoPtr<T>& operator = (AutoPtr<T>& ap)
{
if(_ptr != ap._ptr)
{
if(_ptr)
{
delete _ptr;
}
//管理权转移
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T* operator ->()
{
return _ptr;
}
T& operator *()
{
return *_ptr;
}
//析构函数
~AutoPtr()
{
cout<<"~AutoPtr()"<<endl;
if(_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
void TestAutoPtr()
{
AutoPtr<int> ptr1(new int(10));
AutoPtr<int> ptr2(ptr1);
AutoPtr<int> ptr3(ptr2);
cout<<*ptr3<<endl;
}
auto_ptr主要核心思想就是管理权的转移,当把ptr1赋值给ptr2或者用ptr1拷贝构造ptr2时,把ptr1的指针赋值为NULL,把管理权交给ptr2.
当我们进行简单的操作并不会出现问题:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiQ3chVEa0V3bT9CX5RXa2Fmcn9CXwczLcVmds92czlGZvwVP9EUTDZ0aRJkSwk0LcxGbpZ2LcBDM08CXlpXazRnbvZ2LcRlMMVDT2EWNvwFdu9mZvwleJRlTwIEWlZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39zNzITNxMTM2EDNwgDM3EDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
但是,auto_ptr的思想是管理权的转移,也就是说当用ptr1去拷贝或者赋值给ptr2时,ptr1不再管理这块空间,ptr1会被赋值为NULL,当我们再去访问旧指针时,就会出错,但这并不是我们所期待的。
<2>scoped_ptr:
<span style="color:#000000">template <class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr = NULL)
:_Ptr(ptr)
{}
T* operator->()
{
return _Ptr;
}
T& operator*()
{
return *_Ptr;
}
private:
T* _Ptr;
ScopedPtr<T>(ScopedPtr<T>& ap);
ScopedPtr<T>& operator = (ScopedPtr<T>& ap);
};
void TestScopedPtr()
{
ScopedPtr<int> ptr1(new int(6));
}
int main()
{
TestScopedPtr();
return 0;
}</span>
在aoto_ptr的基础上,我们发现auto_ptr的问题就是拷贝构造和赋值后,再次对旧指针进行操作时会出错。所以,scoped_ptr的思想就是防拷贝,在这里我们模拟实现,把拷贝构造和复制运算符(1)只声明不定义(2)将声明设置为私有。当用户再进行拷贝或者赋值时,程序就会报错,这样就解决了这个问题。但在实际应用场景中,我们是需要进行拷贝或者赋值的,所以这就不满足我们的需求。
<3>shared_ptr:
#include<iostream>
using namespace std;
template <class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL)
:_ptr(ptr)
,_refcount(new int(1))
{}
//ptr2(ptr1)
SharedPtr(const SharedPtr<T>& sp)
{
_ptr = sp._ptr;
_refcount = sp._refcount;
(*_refcount)++;
}
//ptr2 = ptr1
SharedPtr<T>& operator = (const SharedPtr<T>& sp)
{
if(_ptr != sp._ptr)
{
if(--(*_refcount) == 0)
{
delete _ptr;
delete _refcount;
}
_ptr = sp._ptr;
_refcount = sp._refcount;
(*_refcount)++;
}
return *this;
}
T* GetPtr()
{
return _ptr;
}
T* operator->()
{
return _Ptr;
}
T& operator*()
{
return *_Ptr;
}
~SharedPtr()
{
if(--(*_refcount)==0)
{
delete _ptr;
delete _refcount;
}
}
private:
T* _ptr;
T* _refcount;
};
void TestSharedPtr()
{
SharedPtr<int> ptr1(new int(1));
SharedPtr<int> ptr2(ptr1);
SharedPtr<int> ptr3(new int(3));
ptr1 = ptr3;
}
//循环引用
struct ListNode
{
SharedPtr<ListNode> _prev;
SharedPtr<ListNode> _next;
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycleRef()
{
SharedPtr<ListNode> cur = new ListNode;
SharedPtr<ListNode> next = new ListNode;
cur->_next = next;
next->_prev = cur;
}
int main()
{
TestSharedPtr();
return 0;
}
当auto_ptr和scoped_ptr都不能满足我们实际需求时,这是我们引入shared_ptr,它的主要思想是引入了引用计数,使得多个指针可以指向同一块空间,当最后一个指针释放时才真正释放这块空间。
但是,当我们实例化为string [ ]对象时,就会出现问题程序会崩溃,因为string [ ]在开空间时会在头上多开四个字节,以致于记录所开string对象的个数,这样在析构时就知道析构多少次,但是我们析构函数中写的是delete,并不知道它会多开四个字节,就从实际存放对象的地方开始析构,前面四个字节就不会被析构造成内存泄漏,所以导致程序崩溃。所以,我们引入shared_array来解决此问题。
<3>shared_array:
#include<iostream>
#include<string>
using namespace std;
template <class T>
class SharedArray
{
public:
SharedArray(T* ptr)
:_ptr(ptr)
,_refcount(new int(1))
{}
SharedArray(SharedArray<T>& s)
:_ptr(s._ptr)
,_refcount(s._refcount)
{
(*_refcount)++;
}
//传统写法
/*SharedArray<T>& operator = (const SharedArray<T>& s)
{
if(_ptr != s._ptr)
{
if(--(*_refcount)==0)
{
delete[] _ptr;
delete _refcount;
}
_ptr = s._ptr;
_refcount = s._refcount;
(*_refcount)++;
}
return *this;
}*/
//现代写法
SharedArray<T>& operator = (SharedArray<T> s)
{
swap(_ptr,s._ptr);
swap(_refcount,s._refcount);
return *this;
}
int RefCount()
{
return *_refcount;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
~SharedArray()
{
if(--(*_refcount)==0)
{
delete[] _ptr;
delete _refcount;
}
}
private:
T* _ptr;
int* _refcount;
};
void TestSharedArray()
{
SharedArray<int> ptr1(new int[10]);
SharedArray<int> ptr2(ptr1);
SharedArray<string> ptr3(new string[5]);
SharedArray<string> ptr4(ptr3);
ptr3[0] = "111";
ptr3[1] = "222";
}
int main()
{
TestSharedArray();
return 0;
}
这就有效的解决了这个问题。
下面我们先来看一个实例:
struct ListNode
{
SharedPtr<ListNode> _prev;
SharedPtr<ListNode> _next;
ListNode()
:_prev(NULL)
,_next(NULL)
{}
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycleRef()
{
SharedPtr<ListNode> cur = new ListNode;
SharedPtr<ListNode> next = new ListNode;
cur->_next = next;
next->_prev = cur;
}
int main()
{
TestCycleRef();
return 0;
}
调试窗口:
我们看到cur->_next和next->_prev共同管理cur这块空间,所以使得_refcount=2,但是想要cur->_refcoun--,这依赖于next.
next->_pre和cur->_next共同管理next这块空间,所以使得_refcount=2,但是想要next->_refcoun--,这依赖于cur.
也就是说,它们相互依赖对方,谁都释放不了,所以造成了循环引用。为了解决循环引用问题,我们引入weak_ptr.
<4>weak_ptr:
template <class T>
class WeakPtr
{
public:
WeakPtr()
:_ptr(NULL)
{}
WeakPtr(SharedPtr<T>& s)
:_ptr(s.GetPtr())
{}
T* GetPtr()
{
return _ptr;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
struct ListNode
{
WeakPtr<ListNode> _prev;
WeakPtr<ListNode> _next;
~ListNode()
{
cout<<"~ListNode()"<<endl;
}
};
void TestCycleRef()
{
SharedPtr<ListNode> cur = new ListNode;
SharedPtr<ListNode> next = new ListNode;
cur->_next = next;
next->_prev = cur;
}
int main()
{
TestCycleRef();
return 0;
}
这时候的weak_ptr是不会增加shared_ptr的引用计数,这样在释放空间时就不会造成循环引用的问题。
但是,在我们平时使用过程中,如果是支持增加C++11的编译器,就可以直接包#include<memory>,就可以使用相应的函数,既方便又简单。
!