您的位置:首页 > 其它

智能指针之 auto_ptr (pc版与gcc版)

2016-05-11 23:16 309 查看
内存管理一直是令程序员头疼的工作,C++继承了C那高效而又灵活的指针,使用起来稍微的不小心就会导致内存泄漏(memory leak),野指针(wild pointer),访问越界(access denied)等问题。曾几何时,C++程序员无比羡慕JAVE,C#等语言的垃圾回收机制。但C++后来有了更高效的内存管理方法,智能指针的出现,使得C++在内存管理方面做得甚至比JAVE,C#更好。

智能指针(smart pointer)是C++中热门议题,在没有智能指针之前,程序员必须保证new的对象能在正确的时机dalete,四处编写异常捕获代码释放资源。而智能指针则可以在退出作用域时——不管是正常流程离开或是因异常而离开——总能调用delete来析构在堆上动态分配对象。

智能指针有很多种,今天介绍最简单,也是最有名的C++98标准中的“自动指针”std::auto_ptr。

智能指针的思想是,将构造成一个类似于指针的对象,对象在离开作用域时,会自动调动析构函数将对象析构,这就不用我们手动的去delete掉指针了。

auto_ptr的构造函数接受new操作符或者对象工厂创建出的对象指针作为参数,从而代理了原始指针。虽然它是一个对象,但是因为重载了operator*和operator->,其行为非常类似指针,可以把它用在大多数普通指针可用的地方。当退出作用域时,C++语言会保证auto_ptr对象销毁,调用auto _ptr的析构函数,进而使用delete操作符删除原始指针释放资源。

auto_ptr有两个版本,一个是pc版,指针涉及所有权问题;一个是gcc版本,指针不涉及所有权问题。

先讲解pc版本自动指针。下面列出pc版简写的auto_ptr代码:

template<class _Ty>
class auto_ptr
{
public:
auto_ptr(_Ty *_P=0) : _Owns(_P!=0),_Ptr(_P)
{}
auto_ptr(const auto_ptr<_Ty> &_Y):_Owns(_Y._Owns),_Ptr(_Y.release())
{}
auto_ptr<_Ty>& operator=(const auto_ptr<_Ty> &_Y)
{
if(this != &_Y)
{
if(_Ptr != _Y._Ptr)
{
if(_Owns)
delete _Ptr;
}
//if(_Y._Owns)
//    _Owns = true;
_Owns = _Y._Owns;    //转移所有权
_Ptr = _Y.release(); //并将自己的所有权置为"假"
}
return *this;
}
~auto_ptr()
{
if(_Owns)
delete _Ptr;   //所有权不为0,才能释放指针
}
public:
_Ty& operator*()const
{
return *_Ptr;
}
_Ty* operator->()const
{
return _Ptr;
}
public:
_Ty* release()const
{
((auto_ptr<_Ty>*)this)->_Owns = false; //将所有权置为"假"
//_Owns = false;
return _Ptr;
}
private:
bool _Owns;
_Ty *_Ptr;
};


构造函数和析构函数

auto_ptr在构造时获取了某个对象的所有权(ownership),在析构时释放该对象。我们可以这样使用auto_ptr来提高代码安全性:

int *p = new int(0);
auto_ptr<int>ap(p);


我们将指针p交于智能指针ap来管理,且p指针从此失去了对空间管理的所有权,一切操作交于ap来管理,程序员也不必再释放p空间。

这里需要注意几点:

1). 因为指针所有权问题,原始指针p不可以再次释放空间,并且两个auto_ptr不能拥有同一个对象,像这样:

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2(p);


因为ap1与ap2都认为指针p是归它管的,在析构时都试图删除p, 两次删除同一个对象的行为在C++标准中是未定义的。所以我们必须防止这样使用,auto_ptr。

2). 不可用于数组指针的释放

int* pa = new int[10];
auto_ptr<int> ap(pa);


因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。

3). 构造函数的explicit关键词有效阻止从一个“裸”指针隐式转换成auto_ptr类型。

4). C++保证删除一个空指针是安全的。

拷贝构造和赋值

与其他采用引用计数的智能指针不同(如share_ptr),auto_ptr要求其对”裸”指针的完全占有性。也就是说一个”裸“指针不能同时被两个以上的auto_ptr所拥有。那么,在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。

auto_ptr的做法是“所有权转移”,即拷贝或赋值的源对象将失去对“裸”指针的所有权,所以,与一般拷贝构造函数,赋值函数不同, auto_ptr的拷贝构造函数,赋值函数的参数为引用而不是常引用(const reference)。当然,一个auto_ptr也不能同时拥有两个以上的“裸”指针,所以,拷贝或赋值的目标对象将先释放其原来所拥有的对象。

注意一下几点:

1). 因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权,这个时候,对这个auto_ptr的提领(dereference)操作是不安全的。如下:

int* p = new int(0);
auto_ptr<int> ap1(p);
auto_ptr<int> ap2 = ap1;
cout<<*ap1;   //错误,此时ap1只剩null


这种情况较为隐蔽的情形出现在将auto_ptr作为函数参数按值传递,因为在函数调用过程中在函数的作用域中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这样,传入的实参auto_ptr就失去了其对原对象的所有权,而该对象会在函数退出时被局部auto_ptr删除。如下:

void f(auto_ptr<int> ap){cout<<*ap;}
auto_ptr<int> ap1(new int(0));
f(ap1);
cout<<*ap1; //错误,经过f(ap1)函数调用,ap1已经不再拥有任何对象


因为这种情况太隐蔽,太容易出错了,所以auto_ptr作为函数参数按值传递是一定要避免的。

2)我们可以看到拷贝构造函数与赋值函数都提供了一个成员模板在不覆盖“正统”版本的情况下实现auto_ptr的隐式转换。如我们有以下两个类

class base{};

class derived: public base{};

那么下列代码就可以通过,实现从auto_ptr到auto_ptr的隐式转换,因为derived*可以转换成base*类型

auto_ptr ap base = auto_ptr(new derived);

3) 因为auto_ptr不具有值语义(value semantic), 所以auto_ptr不能被用在stl标准容器中。

所谓值语义,是指符合以下条件的类型(假设有类A):

A a1;

A a2(a1);

A a3;

a3 = a1;

那么

a2 == a1, a3 == a1

很明显,auto_ptr不符合上述条件,而我们知道stl标准容器要用到大量的拷贝赋值操作,并且假设其操作的类型必须符合以上条件。

3.提领操作

提领操作有两个操作, 一个是返回其所拥有的对象的引用, 另一个是则实现了通过auto_ptr调用其所拥有的对象的成员。如:

struct A
{
void f();
}
auto_ptr<A> apa(new A);
(*apa).f();
apa->f();


当然,我们首先要确保这个智能指针确实拥有某个对象,否则,这个操作的行为即对空指针的提领是未定义的。

4.辅助函数

1) get用来显式的返回auto_ptr所拥有的对象指针。我们可以发现,标准库提供的auto_ptr既不提供从“裸”指针到auto_ptr的隐式转换(构造函数为explicit),也不提供从auto_ptr到“裸”指针的隐式转换,从使用上来讲可能不那么的灵活,考虑到其所带来的安全性还是值得的。

2) release,用来转移所有权

3) reset,用来接收所有权,如果接收所有权的auto_ptr如果已经拥有某对象,必须先释放该对象。

好了,我们来总结下pc版auto_ptr的一些要点:

1) auto_ptr不能共享所有权,即不要让两个auto_ptr指向同一个对象。

2) auto_ptr不能指向数组,因为auto_ptr在析构的时候只是调用delete,而数组应该要调用delete[]。

3) auto_ptr只是一种简单的智能指针,如有特殊需求,需要使用其他智能指针,比如share_ptr。

4) auto_ptr不能作为容器对象,STL容器中的元素经常要支持拷贝,赋值等操作,在这过程中auto_ptr会传递所有权,那么source与sink元素之间就不等价了。

下来介绍gcc版本的auto_ptr,可以说gcc版本的自动指针是对pc版本做了优化。pc版本的所有权问题对智能指针来说是个经典的问题,但无疑它对于我们操作来说是非常麻烦的。

gcc版本的auto_ptr想法很简单,既然对于这个空间的管理只能是一个指针,那么当把”裸指针”交于auto_ptr管理后,直接将裸指针赋空,不允许你进行非法操作,所有的操作都由我来管理。同样,当两个auto_ptr转让“所有权”时,原auto_ptr也将被赋空,保证有且仅有一个auto_ptr对所指空间进行操作释放,这样就不会顾虑pc版auto_ptr所指空间的二次释放问题!

gcc版的简写auto_ptr代码如下:

template<class _Ty>
class auto_ptr
{
public:
auto_ptr(_Ty *_P=0):_Ptr(_P)
{}
auto_ptr(const auto_ptr<_Ty> &_Y):_Ptr(_Y.release())
{}
auto_ptr& operator=(auto_ptr& __a)
{
if(this != &__a)
{
reset(__a.release());
}
return *this;
}
~auto_ptr()
{delete _Ptr;}
public:
void reset(_Ty* __p = 0)
{
if (__p != _Ptr)
{
delete _Ptr;  //释放原指针所指空间
_Ptr = __p;   //转移
}
}
_Ty& operator*()const
{
return (*get());
}
_Ty* operator->()const
{
return (get());
}
_Ty* get()const
{return _Ptr;}
_Ty* release()const
{
_Ty* __tmp = _Ptr;  //定义临时变量
((auto_ptr<_Ty>*)this)->_Ptr = 0; //释放原指针
return __tmp;       //转移“所有权”
}

private:
_Ty *_Ptr;
};


对比两个版本的auto_ptr代码,可见gcc版对指针的所有权问题上跟pc版的差异。可以这么认为,正是因为gcc版本不涉及所有权,所以”所有权”才转移的更加彻底,避免了很多麻烦(空间释放等)。

但也不是说pc版的auto_ptr一无是处,所有权的思想非常重要,我们在接下来学习其他的“更高级”的智能指针时还要用到。

同样的,gcc版虽然”一劳永逸”,但实际上这种优化可能与用户所要求的有偏差,虽然我们原指针交由智能指针操作,但我们对原指针也应该能够进行原本应该进行的操作,比如读取,赋值,删除等操作。但是预防二次释放的问题应该怎么解决呢?

事实上auto_ptr只是最初级的智能指针,更高级的智能指针能够完美解决这些问题,我们下期再来分析!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: