c++中的智能指针auto_ptr解析

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采用了特化的处理,而在处理拷贝语义上采用了设置计数器的技术,这里不做详细说明。




 

你可能感兴趣的:(C++,auto_ptr)