c++中的auto_ptr是一个类,却可以像指针一样去使用。使用auto_ptr需要包含头文件#include <memory>
例如:auto_ptr<string> ps(new string("hello"));可以像指针一样去使用它,可以这样cout << ps->size()<<endl;uto_ptr带来的好处是,程序员并不需要手动的去delete,这给容易忘记delete的程序员带来极大的好处,因为有时候delete真的是很容易忘记,而内存泄漏又是很难被发现的错误。java中采用了垃圾回收器的机制,使其很大程度上避免了内存泄漏的问题。
引例:
#include <iostream> #include <memory> using namespace std; class A{ public: A(int x = 0) :m_x(x){ cout << "A构造" << endl; } ~A(){ cout << "A析构" << endl; } void print()const{ cout << m_x << endl; } private: int m_x; }; void test(){ A * pa = new A(123); pa->print(); delete pa; } int main(){ cout << "main函数开始了" << endl; test(); cout << "main函数结束" << endl; }
上面的代码中,在test函数中如果我们忘记了delete,也就是将delete pa注释掉,则A类的析构函数是无法被调用的。在test函数中,A * pa是一个局部指针变量,函数结束pa是会被释放掉的,但是pa所指向的内存空间是无法被释放的。所以为了避免程序员忘记delete而造成内存泄漏,auto_ptr的思想就在于,能不能让pa所指向的内存空间伴随者pa的释放而被释放掉。试想一下,如果pa是一个类类型,且pa是分配在栈内存上,则pa被释放后,pa的析构函数是一定会被调用的。那么就在pa类的析构函数中释放pa所指向的堆内存空间,那么就不用手动delete了,pa就是auto_ptr。
所以如果我们把上面的代码中的A * pa = new A(123)注释掉,加上这样一句auto_ptr<A> pa(new A(123)),且把delete pa也注释掉,则会发现,A类的析构函数被调用了。
查看一下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;
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
}
/*在这里代码有截断*/
~auto_ptr() _NOEXCEPT
{ // destroy the object
delete _Myptr;
}
/*在这里代码被截断*/
private:
_Ty *_Myptr;
// the wrapped object pointer
可以看出auto_ptr是用了类模板,可以接受任意引用类型的参数。以auto_ptr<string> ps(new string("hello"))这句话为例,执行这句话的时候,调用了auto_ptr的第一个构造函数,将new出来的地址给了_Ty *_Myptr,这里的_Ty已经被实例化成了string了。而且构造函数还加了explicit防止类型转换,也就是说这样写auto_ptr<string> ps = new string("hello")会报错。而且可以看到最后的析构函数将这个new出来的内存delete了,所以在使用auto_ptr的时候不用程序员手动释放了。
atuo_ptr的缺陷:
缺点1:auto_ptr缺乏拷贝语义。
c++中接触到的拷贝一般有三种,深拷贝,浅拷贝,转移拷贝。
a、浅拷贝:
就是逐字节拷贝,默认拷贝构造函数就是浅拷贝。直接将一个指针赋值给另一个指针,导致两个指针指向同一块内存。
b、深拷贝
一个指针赋值给另一个指针的同时,进行了内存的处理,两个指针有自己独立的内存空间。
c、转移拷贝
一个指针赋值给另一个指针之后,前一个指针(源指针被赋值为空)。
auto_ptr采用的就是转移语义的拷贝,会产生什么结果呢?我们把第一个例子的test函数改成这种样子。
void test(){ /*pa本身会被释放,一种机制使得pa的释放导致堆内存的释放 所以不再让一个普通指针来保存pa,用一个类来保存 */ //A * pa = new A(123); auto_ptr<A> pa(new A(123)); pa->print(); //delete pa; /*智能指针的问题,缺乏拷贝语义,普通指针肯定没问题*/ auto_ptr<A> pb = pa;//拷贝构造 pb->print(); /*段错误*/ pa->print(); }
那么最后一句话pa->print()的执行将发生段错误,因为这个时候pa已经为空了,通过空指针去访问必然发生段错误。这里和普通指针的访问是不一样的,就是因为其拷贝方式特殊。
上面的三种拷贝语义,auto_ptr为什么没有使用浅拷贝语义或者深拷贝语义?如果用浅拷贝语义,那么在释放的时候必然出现对同一个指针的doulle free的问题,就是释放了两次。那么深拷贝呢?就是为拷贝对指针赋予独立的内存空间,这样做的问题是不符合指针拷贝的逻辑,因为一个指针拷贝给另外一个指针,两个指针还是指向的同一块内存,所以也没考虑深拷贝,最后就选择了一种转移拷贝的方式。
缺点2:不能管理对象数组
设计函数test2:
/*智能指针管理多个对象的问题*/ void test2(){ auto_ptr<A> pa(new A[3]);//核心转存,解映射出错 /*为什么智能指针不能管理对象数组*/ }
为什么auto_ptr不能管理对象数组呢?
一次分配多个对象我们必须要用delete[]去释放,不然会出错。最终的问题其实就是delete和delete[]的区别问题。
delete用于释放单个对象,获取的是对象的地址,并且调用free,而对象数组的分配有所不同,除了分配所需要大小的堆内存空间外,这段堆内存的开头还分配了4个字节的空间,用于存放分配对象的个数,在释放的时候必须把最开头的四个字节包含进去,delete[]在释放的时候,将得到的地址减4,这样取得了最开始的地址,这样的释放才是正确的释放,而delete的释放并没有减4的操作,所以无法正确的释放。
可以看到auto_ptr的析构函数中只有delete的释放,所以对象数组必然出错。
改进的auto_ptr使其能够管理对象数组。我们将对象数组特殊化处理,在这里面采用delete[]来释放,采用类模板的特化处理;
改进的版本,可以管理对象数组
#include <iostream> using namespace std; template<typename T> class AutoPtr{ public: explicit AutoPtr(T*p = NULL) :m_p(p){} ~AutoPtr(){ if (m_p) delete m_p; } /*拷贝构造*/ AutoPtr(AutoPtr<T>& that) :m_p(that.release()){} /*拷贝赋值*/ AutoPtr& operator=(AutoPtr<T>& that){ if (&that != this) reset(that.release()); return *this; } T& operator* ()const{ return *m_p; } T* operator->()const{ return &**this; //return this->m_p; } private: T* release(){ T* p = m_p; m_p = NULL; return p; } /*重置m_p*/ void reset(T* p){ if (p != m_p){ delete m_p; m_p = p; } } T * m_p; }; /*特化的版本,全部重写的方式*/ template<typename T> class AutoPtr<T[]>{ public: explicit AutoPtr(T*p = NULL) :m_p(p){} ~AutoPtr(){ if (m_p) delete[] m_p; } /*拷贝构造*/ AutoPtr(AutoPtr<T>& that) :m_p(that.release()){} /*拷贝赋值*/ AutoPtr& operator=(AutoPtr<T>& that){ if (&that != this) reset(that.release()); return *this; } T& operator* ()const{ return *m_p; } T* operator->()const{ return &**this; //return this->m_p; } private: /*转移拷贝语义的实现函数1,把指针置为空,然后返回*/ T* release(){ T* p = m_p; m_p = NULL; return p; } /*重置m_p*/ void reset(T* p){ if (p != m_p){ delete m_p; m_p = p; } } T * m_p; }; class A{ public: A(int x = 0) :m_x(x){ cout << "A构造" << endl; } ~A(){ cout << "A析构" << endl; } void print()const{ cout << m_x << endl; } private: int m_x; }; void test(){ AutoPtr<A> pa(new A(123)); pa->print(); (*pa).print(); AutoPtr<A> pb = pa; pb->print(); pa = pb; (*pa).print(); } void test2(){ AutoPtr<A[]> p(new A[3]); } int main(){ test(); test2(); }
上面代码的改进不仅是使auto_ptr具有原来的功能,还能管理对象数组,将对象数组做了特殊化处理。
值得说明的是在c++2011中出现的smart_ptr指针,真正做到了“智能”,其不仅可以管理对象数组,还具有指针的拷贝语义,在管理对象数组上smart_ptr采用了特化的处理,而在处理拷贝语义上采用了设置计数器的技术,这里不做详细说明。