个人博客传送门
智能指针是C++中一个编程技巧。它保证内存的正确释放,解决了内存泄漏的问题。有一个思想叫做RAII,RAII指的是资源分配即初始化。我们通常会定义一个类来封装资源的分配和释放,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理。
在C++中,我们一般是使用new和delete来实现内存的初始化和释放。正确的配对使用可以处理绝大部分问题,但是如果出现了执行流的跳转,比如:语句中间出现了return,break,continue,goto,等关键字,我们的delete可能没有执行,这样就导致了内存没有被释放。又或者我们的语句出现了错误,抛出异常导致程序结束,也有可能没有执行delete语句。这 些问题都导致了内存泄漏。智能指针的出现就是为了解决这些问题的。智能指针其实是一个类,它可以自动的处理指针指向的动态资源的释放。
发展历程
- 早期C++98:auto_ptr,最早出现的智能指针,拷贝机制是管理权转移,致命缺陷,不使用。
- boost(非官方):
- scoped_ptr/scoped_array:守卫指针,拷贝机制是不拷贝,简单粗暴
- shared_ptr/shared_array:共享指针,拷贝机制是引用计数,比较复杂,会出现循环引用的问题。
- weak_ptr:弱指针,不单独使用,辅助共享指针解决循环引用的问题
- C++11:unique_ptr对应boost的scoped_ptr;shared_ptr对应boost的shared_ptr;weak_ptr对应boost的weak_ptr。
本文主要实现shared_ptr和weak_ptr的模拟,auto_ptr和scoped_ptr模拟拷贝相关的函数。
auto_ptr
//模拟主要函数:
template <class T>
class AutoPtr{
private:
T* _ptr;
public:
AutoPtr(T* ptr){ _ptr = ptr; }
~AutoPtr(){ delete _ptr; }
T& operator*(){ return *_ptr; }
T* operator->(){ return _ptr; }
AutoPtr(AutoPtr<T>& ap){
//管理权转移
_ptr = ap._ptr;
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap){
//自己给自己赋值不作处理
if(this != &ap){
if(_ptr)
delete _ptr;
//管理权转移
_ptr = ap._ptr;
ap_ptr = NULL;
}
return *this;
}
};
int main(){
AutoPtr<int> ap1(new int(10));
AutoPtr<int>ap2 = ap1;
//崩溃,因为ap1已经指向NULL
*ap1 = 20;
return 0;
}
图解如下:
任何时候我们都不使用auto_ptr,因为管理权的转移是不符合我们正常指针的使用的,而且会引起程序崩溃,这个是不允许的。
最后,解释一下,operator->()
返回T*
的原因:
//设定一个类
struct student{ int num; }
//调用
AutoPtr<student> sp = new student;
sp->num = 20;
sp->num
等价于sp.operator->()
。sp.operaotr->()
返回T*
指针之后,编译器自动将原式优化为_ptr->num
,从而实现对元素的访问。
scoped_ptr
防拷贝的智能指针,boost版本相当于C++11的unique_ptr
//模拟拷贝的主要函数:
template <class T>
class ScopedPtr{
private:
//与AutoPtr不同的就是这两个函数
ScopedPtr(const ScopedPtr<T>& sp);
ScopedPtr<T>& operator= (const ScopedPtr<T>& sp);
};
scoped_ptr通过将拷贝构造函数和赋值运算符重载定义为私有,同时只声明不定义,这样可以保证该类不能被拷贝。这就简单的解决了auto_ptr因为拷贝导致的管理权转移问题。
shared_ptr
引用计数的智能指针,这个类除了有指针之外,再多开辟了一个内存空间,用于存放计数。这种智能指针是实现的最好的,在boost库中,实现起来很复杂,因为要考虑多线程等情况。
//模拟主要函数
template <class T>
class SharedPtr{
private:
T* _ptr;
int* _refcount;
public:
//构造
SharedPtr(const T& ptr){
_ptr = ptr;
_refcount = new int(1);
}
//析构
~SharedPtr(){
if(--_refcount == 0){
delete _ptr;
delete _refcount;
}
}
//拷贝构造
SharedPtr(SharedPtr<T>& sp){
_ptr = sp._ptr;
_refcount = sp._refcount;
++_refcount;
}
//赋值运算符重载
SharedPtr<T>& operator=(const SharedPtr<T>& sp){
if(*this != &sp){
if(--(*_refcount) == 0){
delete _ptr;
delete _refcount;
}
_ptr = sp._ptr;
_refcount = sp._refcount;
(*_refcount)++;
}
return *this;
}
//*重载
T& operator*(){ return *_ptr; }
//->重载
T* operator->(){ return _ptr; }
};
这个实现,将是比较实用的。但是依然有一个场景下,会出现问题。这个问题叫做循环引用,问题的根源是引用计数被循环使用,不能减为0,导致死循环。下面以双向链表作为例子:
//定义一个链表节点如下
struct ListNode{
//构造函数...
//为了方便调用,设为public
int _data;
SharedPtr<ListNode> _next;
SharedPtr<ListNode> _prev;
};
//调用这个节点,设定这样一个场景
int main(){
SharedPtr<ListNode> cur(new ListNode);
SharedPtr<ListNode> next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
创建模型如下:
根据上面例子,_next
和next
都指向后面节点这个空间,next._refcount = 2
。_prev
和cur
指向前面那个节点的空间,cur._refcount = 2
。当程序结束的时候,next
先被析构。若需要析构next
就需要析构next._prev
;要析构next._prev
就需要析构cur
;要析构cur
就需要析构cur._next
;要析构cur._next
就需要析构next
……这样就造成了死循环。为了解决这个循环引用的问题,引入了弱指针weak_ptr。
weak_ptr
弱指针不单独使用,它的存在就是为了解决使用shared_ptr造成的循环引用问题。
template <class T>
class WeakPtr{
private:
T* _ptr;
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;
}
};
//还需要修改一下两处:
//1、修改ListNode的结构
struct ListNode{
int _data;
WeakPtr<ListNode> _next;
WeakPtr<ListNode> _prev;
};
//2、将WeakPtr定义为SharedPtr的友元,因为WeakPtr中需要访问SharedPtr的私有成员
template <class T>
class SharedPtr{
friend class WeakPtr;
//...
};
这样,上面那个例子中,next._refcount = 1
,cur._refcount = 1
,就不会出现循环引用的问题了。