目录
在C++中,是通过new和delete来进行动态内存管理的。使用new从堆上分配空间,使用delete来释放空间。由于这两个工作都不是自动进行的,因此动态内存的管理是很困难的,因为无法确保能在正确的时间对已申请的动态内存进行释放:有可能申请之后忘记释放,这样就会造成内存泄漏;也有可能提前就进行了释放,这就会引起后续的非法访问操作。
智能指针,就是为了更简单、更安全地管理动态内存。智能指针是基于“以对象管理资源”的观念,主要有两个关键点:获得资源后立刻放进管理对象(也就是“资源取得时机便是初始化时机”,即RAII),并且管理对象调用析构函数时确保资源被释放。这里所说的“管理对象”,就是智能指针。实际上,智能指针就是具有指针行为的对象。
(以下源码均源于VS2013自带STL)
auto_ptr
auto_ptr虽然在C++11中已经被弃用,但是通过它来理解智能指针还是非常有帮助的,它定义在xmemory文件中。
构造函数
auto_ptr类提供了三种构造方式:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0() //通过指针构造
: _Myptr(_Ptr)
{ // construct from object pointer
}
auto_ptr(_Myt& _Right) _THROW0() //拷贝构造
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
template<class _Other>
auto_ptr(auto_ptr<_Other>& _Right) _THROW0() //不同模板类型auto_ptr对象构造
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right
}
......
private:
_Ty *_Myptr; // the wrapped object pointer
};
三者分别如下所示:
int * x = new int(3);
std::auto_ptr<int> ptr0(x); //第一种构造
std::auto_ptr<int> ptr1(ptr0); //第二种构造
std::auto_ptr<const int> ptr2(ptr0); //第三种构造
对于第一种构造,是直接通过一个指针来构造,用传入的指针初始化成员指针_Myptr变量;
对于第二种构造,是通过另一个auto_ptr对象来构造,并用传入的对象调用release函数的返回值来初始化_Myptr;
对于第三种构造,是通过不同模板类型的auto_ptr对象来构造,也是将传入的对象调用release函数的返回值来初始化_Myptr。
auto_ptr的成员函数release定义如下:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
......
_Ty *release() _THROW0()
{ // return wrapped pointer and give up ownership
_Ty *_Tmp = _Myptr;
_Myptr = 0;
return (_Tmp);
}
......
private:
_Ty *_Myptr; // the wrapped object pointer
};
在release函数中,会先保存指针成员_Myptr的值到_Tmp中,然后将_Myptr置0后,返回_Tmp。回到构造函数中,当使用auto_ptr对象来构造时,传入的对象参数的_Myptr就会被置为0,也就是NULL,而它原来的值则会被保留到新构造的对象中。相当于换了一个指针指向原地址。这一点充分体现了auto_ptr的要求:一个物件只能有一个拥有者,严禁一物二主(《C++标准程序库》)。
拷贝赋值
auto_ptr通过重载赋值运算符来实现拷贝赋值。赋值重载定义如下:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
......
template<class _Other>
_Myt& operator=(auto_ptr<_Other>& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
_Myt& operator=(_Myt& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
......
private:
_Ty *_Myptr; // the wrapped object pointer
};
和前面的构造类似,这里的拷贝赋值也重载了两种:一种是相同模板类型下auto_ptr对象的拷贝赋值,另一种则是不同模板类型下auto_ptr对象的拷贝赋值。这两种的实现都是相同的,都会调用release函数,相当于释放了传入对象的拥有权,并将拥有权转移到被赋值的左值对象上。这里还调用了一个reset函数,该函数定义如下:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
......
void reset(_Ty *_Ptr = 0)
{ // destroy designated object and store new pointer
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}
private:
_Ty *_Myptr; // the wrapped object pointer
};
reset函数的作用很简单:如果传入的指针与当前的_Myptr指针不同,那么就释放_Myptr所指向的内存,不管是否相同,最后都会将传入的指针参数赋值给_Myptr。这就相当于让auto_ptr的指针成员_Myptr指向传入指针参数所指向的地方。
也就是说,拷贝赋值的作用,就是让当前auto_ptr指向传入赋值的参数指向的地方,并且重置该参数指针为NULL,相当于拥有权的转移,也体现了“一个物件只能有一个拥有者”的设计思想。
这里说了一个“让auto_ptr指向的地方”,显然,auto_ptr只是一个类/对象,它的“指向”都是通过其成员指针变量指针_Myptr实现的,按道理来说auto_ptr不应该有“指向”的说法,但是auto_ptr又却是被称为智能指针,这就是因为作为类/对象的auto_ptr确实能有指针一样的行为。
让auto_ptr对象具有指针的行为
简单来说,就是要让auto_ptr对象用起来像指针。举个例子,定义了一个auto_ptr对象:auto_ptr<type>p(new type());通过*p可以访问到用来初始化的type类型的对象,并且p->xxx则可以访问用于初始化的type类型的对象中的成员。可以看到,这两种方式完全把p当做了一个指针来使用,并且这个指针就指向用来初始化auto_ptr的参数。
前面说过,auto_ptr的指针行为,都是通过指针_Myptr来实现的,因此,要想让*p和p->有意义,只需要指针_Myptr来重载“*”和“->”即可。auto_ptr是这样做的:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
......
_Ty& operator*() const _THROW0()
{ // return designated value
return (*get());
}
_Ty *operator->() const _THROW0()
{ // return pointer to class object
return (get());
}
_Ty *get() const _THROW0()
{ // return wrapped pointer
return (_Myptr);
}
......
private:
_Ty *_Myptr; // the wrapped object pointer
};
先来看这里有一个get函数,调用该函数得到_Myptr的值。重载“*”,使得“*p”返回的实际上是*_Myptr,而“p->”返回的则是_Myptr本身。举个例子,如果auto_ptr的模板类型为class A,那么_Myptr就是指向一个A object的指针,那么*_Myptr就是这个object,因此就会有以下结果:
由图可知,重载“*”和“->”,就可以通过auto_ptr对象去访问它所指向的对象,当然,如果auto_ptr本身模板类型就是int、double这样的,那么(*p)就是对应的int型变量的值,而p->理应是int型变量的地址,但是由于“->”很特殊,因此光是一个“p->”并不能通过编译,需要“p.operator->()”才是int型变量的地址。
这样,就使得auto_ptr的行为更像指针。
析构函数
智能指针作为“用对象管理资源”的工具,需要实现两个关键:在资源获取时就是初始化,这一点在构造函数中得以体现;第二点则是在析构函数中释放资源。下面就来看看auto_ptr的析构函数:
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
......
~auto_ptr() _NOEXCEPT
{ // destroy the object
delete _Myptr;
}
......
private:
_Ty *_Myptr; // the wrapped object pointer
};
在析构函数中,只做了一件事:释放_Myptr指向的地址。这样,就保证了资源在auto_ptr对象构造时获取,在auto_ptr对象析构时释放,实现“智能”管理资源。
由上可知,auto_ptr是一种独占性的指针,但是它最大的问题在于:向用户提供了拷贝、赋值等操作,而这些操作的最后都会把原对象持有的指针置为NULL,却不给用户任何提示。这就会导致用户不小心对auto_ptr对象进行了拷贝、赋值等操作,却没有任何提示,如果用户又不小心使用了原对象,那么就很有可能引起问题,而这种问题是很难发现的。并且从另方面来说,既然设计原则是“严禁一物二主”,那么为何还提供拷贝、赋值这样的操作呢?这显然也是不符合“独占”语义的,更符合“交换拥有权”的语义,而unique_ptr则解决了这一问题。
unique_ptr
unique_ptr是auto_ptr从C++11开始的一种替代品,它也延续了auto_ptr“严禁一物二主”的原则,不过unique_ptr更符合语义的一点,是直接禁止了拷贝构造和赋值重载,如下所示:
template<class _Ty,class _Dx>
class unique_ptr
{
......
unique_ptr(const _Myt&) = delete; //禁用拷贝构造
_Myt& operator=(const _Myt&) = delete; //禁用赋值重载
}
在分析unique_ptr源码之前,还有一些是需要知道的。
unique_ptr与auto_ptr相比,