要学习智能指针之前需要先了解一下什么叫RAII?
所谓RAII就是运行时初始化,具体的实现就是定义一个类来封装运行资源的分配和释放工作,在构造函数中进行资源的分配,在析构函数中进行资源的回收工作,这样就可以保证资源能够正确的分配和释放。
智能指针
光看名字你可能会以为,智能指针的充其量不过就是一个指针罢了,那么智能指针和我们平时所使用的普通指针有什么区别?大家都知道在C++中使用指针来指向一个对象或者管理一块空间时,当我们使用完之后总是要手动的去释放该指针所管理的这块空间;而如果我们使用的是智能指针来管理一块空间,那么智能指针就会为我们动态的来管理这块空间,所以的回收工作也会有析构函数来为我们完成。
在刚开始时我们所使用的智能指针都是来自boost社区提供的boost库中的智能指针,直到C++11发布后,库中才提供了相应的unique_ptr/shared_ptr/weak_ptr,下图为boost库中的智能指针
智能指针的模拟实现(为了和库中的智能指针做区分在命名做相应的改动)
auto_ptr
这种智能指针从本质上来说是进行资源的转移,先用一个智能指针管理一块空间,再次将该空间交给另外一个智能指针进行管理时,原来的指针必须切断与该空间的关联,将它的管理权转交给新的管理的智能指针,这样显然是不合理,同时也容易出现错误,通常情况下不要使用这种智能指针。
图示:
模拟实现(旧库中的写法):
template
class AutoPtr
{
public:
AutoPtr()
{}
AutoPtr(T* p)
: _p(p)
, _owner(false) //防止指针开始时指向的空间为空
{
if (_p) //如果指针所指的空间不为空,则由它管理该空间
_owner = true;
}
AutoPtr(AutoPtr& ap)
: _p(ap._p)
, _owner(ap._owner)
{
ap._owner = false;
}
AutoPtr& operator=(AutoPtr& ap)
{
if (_p != ap._p)
{
if (_owner)
{
delete _p;
}
_p = ap._p;
_owner = true;
ap._owner = false;
}
return *this;
}
~AutoPtr()
{
//释放时先看指针所指向的空间是否为空,在看空间的使用者是否为0
if (_p != NULL)
{
delete _p;
_p = NULL;
_owner = false;
}
}
T& operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
T* Get()
{
return _p;
}
protected:
T* _p;
bool _owner;
};
template
class AutoPtr
{
public:
AutoPtr()
{}
AutoPtr(T* p)
:_p(p)
{}
AutoPtr(AutoPtr& ap)
: _p(ap._p)
{
ap._p = NULL;
}
//只适用于将智能指针用于管理一段空间的情况
//如果一段空间被多个对象来管理时就会出错
AutoPtr& operator=(AutoPtr& ap)
{
if (this != &ap)
{
}
return *this;
}
~AutoPtr()
{
if (_p != NULL)
{
delete _p;
_p = NULL;
}
}
T& operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
T* Get()
{
return _p;
}
protected:
T* _p;
};
void Fun()
{
int* p = new int;
AutoPtr ap1(p);
AutoPtr ap2(ap1);
AutoPtr ap3;
ap3 = ap1;
}
运行结果:
当程序执行完析构时程序就会发生崩溃,因为ap1拷贝构造出ap2时就会失去对来空间的控制权,在析构时ap1就会变成野指针。要解决这种情况就需要引入我们下面要引入的防拷贝的智能指针。
scoped_ptr/unique_ptr
scoped_ptr是boost库中实现的,unique_ptr是C++标准库中实现的,这两个指针为了解决auto_ptr指针资源转移后失去对原来空间管理的问题,我们称之为防拷贝的智能指针,防拷贝就是防止对象之间进行拷贝构造或者相互赋值。那么如何可以实现防拷贝是我们不得不考虑的问题?
防拷贝必须满足下面两个条件:
1)只给出函数的声明而不实现——防止友元函数对类中的私有函数进行访问;
2)将这两个函数的声明给成私有的——这样是为了防止函数在类外进行函数体的实现;
代码实现:
template
class ScopedPtr
{
public:
ScopedPtr()
{}
ScopedPtr(T* ptr)
: _p(ptr)
{}
~ScopedPtr()
{
if (_p != NULL)
delete _p;
}
T& operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
T* Get()
{
return _p;
}
private:
ScopedPtr(const ScopedPtr& sp);
ScopedPtr& operator=(const ScopedPtr& sp);
T* _p;
};
void FunTest()
{
ScopedPtr sp1;
//ScopedPtr sp2(sp1); //因为是防拷贝的所以该句编译出错;
ScopedPtr sp3;
//sp3 = sp1; //编译同样会出错,因为赋值运算符的重载也是防拷贝的
}
scoped_array模拟实现
template
class ScopedArray
{
public:
ScopedArray()
{}
ScopedArray(T* ptr)
: _p(ptr)
{}
~ScopedArray()
{
if (_p != NULL)
{
delete[] _p;
_p = NULL;
}
}
T operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
T* Get()
{
return _p;
}
T& operator[](size_t index) //对于[]这个运算符需要成对的重载
{
return _p[index];
}
const T& operator[](size_t index)const
{
return _p[index];
}
private:
ScopedArray(const ScopedArray& sp);
ScopedArray& operator=(const ScopedArray& sp);
T* _p;
};
有的时候在智能指针中我们就需要智能指针之间的相互赋值或者实现拷贝构造的功能,显然上面的这种防拷贝的智能指针显然是不符合我们的要求,因此我们引入了shared_ptr这种类型的智能指针,而这种智能指针相比于第一种auto_ptr指针,主要使用的是引用计数的这种来实现的,当一块空间被一个指针指向时,该空间的引用计数进行加1,当指向的这块空间的指针少一个时,指向该空间的引用计数就减1,而只有当指向该空间的引用计数为0时,该空间才能被释放。
普通版的shared_ptr智能指针实现:
template
class SharedPtr
{
public:
SharedPtr()
{}
SharedPtr(T* ptr)
: _p(ptr)
, _pCount(NULL)
{
if (_p)//防止指针刚开始指向的内存空间为空
_pCount = new int;
}
SharedPtr(const SharedPtr& sp)
: _p(sp._p)
, _pCount(sp._pCount)
{
_p = sp._p;
_pCount = sp._pCount;
++(*_pCount);
}
SharedPtr& operator=(SharedPtr sp)
{
swap(sp._p, _p);
swap(sp._pCount, _pCount);
return *this;
}
~SharedPtr()
{
if (NULL != _p && 0 == --(*_pCount))
{
delete _p;
delete _pCount;
_p = NULL;
_pCount = NULL;
}
}
T& operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
T* Get()
{
return _p;
}
int GetUseCount()
{
return *_pCount;
}
private:
T* _p;
int* _pCount;
};
所谓的定制删除器主要是针对析构函数来说的,因为在上面析构函数中只是简单的进行单个指针的释放,但是在如果是针对打开的文件或者是指向的连续存储的对象时显然是不满足这种情况,因此对于不同的情况需要不同的析构函数进行处理,而C++标准库中正是采用这种定制删除器的方式;
定制删除器代码实现:
class Fclose
{
public:
void operator()(void *ptr)
{
fclose((FILE *)ptr);
cout << "fclose()" << endl;
}
};
struct Free
{
public:
void operator()(void *ptr)
{
free(ptr);
cout << "free()" << endl;
}
};
//默认删除器是delete
class DefaultDel
{
public:
void operator()(void* ptr)
{
delete ptr;
cout << "delete ptr" << endl;
}
};
template
class SharedPtr
{
public:
SharedPtr(T* ptr, D del = DefaultDel())
SharedPtr::SharedPtr(T* ptr, D del)
:_ptr(ptr)
, _pCount(new int(1))
, _del(del)
{}
SharedPtr(const SharedPtr& sp)
{
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
SharedPtr& operator=(SharedPtr sp)
{
std::swap(sp._ptr, _ptr);
std::swap(sp._pCount, _pCount);
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~SharedPtr()
{
if (--(*_pCount) == 0)
{
_del(_ptr);
delete _pCount;
_ptr = NULL;
_pCount = NULL;
}
}
int Count()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
D _del;
};
循环引用问题:在一些特殊场景下上面的这种智能指针存在着一种致命的问题,比如当我们创建一个双向链表时,链表中的指针全部都采用这种shared_ptr这种智能指针来管理,那么当我们在最后释放时就会产生类似于死锁中一个等一个释放的尴尬情况,而这种情况无疑是所有的对象都不能得到释放;
图示举例:
简单的代码举例:
struct Node
{
int _data;
shared_ptr _next;
shared_ptr _prev;
}
假设现在先创建两个结点,并用shared_ptr维护这两个结点:
shared_ptr sp1(new Node);
shared_ptr sp2(new Node);
现在将这两个指针互相连接:
sp1->_next=sp2;
sp2->prev=sp1;
weak_ptr主要是用来解决shared_ptr指针的循环引用的问题,weak_ptr我们称之为弱指针,用来辅助shared_ptr,这时因为weak_ptr是不能单独使用的,需要有别的智能指针进行辅佐,weak_ptr是一种不控制所指向对象生存周期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一但最后一个指向对象的shared_ptr被销毁,对象就会被销毁,即使有weak_ptr指向对象,对象还是会被释放。
简单代码举例:
struct Node
{
int _data;
weak_ptr _next;
weak_ptr _prev;
}