由一份auto_ptr源代码所引发的思考

时间:2020-11-28 15:06:30

由一份auto_ptr源代码所引发的思考
                               Kyle

                        CPPCN 版权所有
如果我问你,auto_ptr最关键的地方,或者说它与一般指针最不一样的地方在哪里,你一定会说它是一个对象,它与自己所占有的资源密切相关,当自己消亡的时候,它也会将自己所拥有的资源一同释放。很好,它之所以会拥有这一特性,全都要归于析构函数的功劳,在析构函数中,它会进行善后处理,这样也就很好的避免了资源泄漏。当然,引入auto_ptr的原因还不至于这么简单,因为许多资源泄漏的问题主要是由于我们的粗心大意所致,所以只要我们细心一些,还是会避免一些不该发生的资源泄漏问题。那究竟是什么原因让我们引入auto_ptr呢?对了,你一定也想到了,就是在异常处理的时候。Ok,来看下面这个例子:
void foo()
{
 ClassA *ptr=new ClassA;
 try
 {
/*  此处放置可能抛出异常的操作 */
 }
 catch(…)
 {
   delete ptr;
   throw;
 }
 delete ptr;
}
上面这个例子使用了一般的指针,你会发现它防止发生资源泄漏的处理是多么复杂,必须在每个catch中进行资源的释放,而且有可能会有许多的catch。天哪,这简直就是灾难,它会使我们的代码很长,不易维护,如果忘记其中的一个,就会产生莫名其妙的错误。既然一般的指针防止资源泄漏会如此的繁琐,那有没有一个办法可以使我们不必操心资源的释放呢,由于在异常发生的时候,会进行堆栈解退,所以我们不必担心作为局部变量的指针本身不被销毁,既然这样,我们干脆建立一个指针对象,好比下面这样:
template<class T>
class auto_ptr1
{
private:
 T* ap;
public:
   ……..
 ~auto_ptr(); //资源释放
}
当指针被销毁的时候,必然会执行析构函数,那就在析构函数中进行资源的释放不就ok了,呵呵,怎么样,是不是很简单呢?的确,整个逻辑的确很简单,但是如果我们在深入思考一下这个指针对象的特性的话,我们会发现有一个困难的问题等待我们去解决。那下面就让我们来看看会遇到什么困难。
   由于在auto_ptr销毁的时候它会自动通过析构函数释放所拥有的资源,那么也就决定了auto_ptr对于资源的独占性,即一个资源只能被一个auto_ptr所指向。这一点应该很好理解,假设有两个auto_ptr指向同一个资源,那当其中一个被销毁的时候,另一个将会指向哪里呢?这种指针往往是最为危险的。既然这样,我们怎样才能保证这种独占性呢,其实也很简单,当对指针进行赋值和复制的时候,剥夺原有指针对资源的拥有权,问题也就迎韧而解了。就好比这样:
 auto_ptr<int> p(new int(20));
 auto_ptr<int> q;
 q=p;  //p已经丧失了对资源的拥有权,q现在是p的主人

对于这种一般性的情况,问题似乎已经解决了,下面让我们来看一种特殊但却合理的情况:

auto_ptr<int> foo()
{
 auto_ptr<int> p(new int(20));
 return p;
}
int main()
{
 auto_ptr<int> q(foo());
 return 0;
}
你认为上面这种情况怎么样,它是合理的,因为它实现了资源的顺利移交,但是你认为auto_ptr<int> q(foo());这一句应该怎样才能调用成功呢?为了说清这个问题,还需要说说左值和右值以及临时对象的问题。也许你会说左值应该就是能够改变的变量,右值当然就是不能改变的变量喽!对吗?对了一点点,实际上左值是能够被引用的变量,说的通俗点就是有名字的变量,你一定想到了些什么,对了,临时变量就没有名字,即使有你也不会知道,因为它不是由你创建的,编译器会在内部辨别它,但你并不知道,因此临时变量不是左值,而是右值。你也许还会问,那const变量是不是左值呢?根据定义,它有名字,当然就是左值了。因此左值并非一定“可被修改”。但是左值和右值与参数有什么关系吗?我要告诉你的是:有,而且相当密切,因为标准c++规定:若传递给类型为引用的形参的实参是右值的话必须保证形参为const引用。Ok,现在让我们回到原来的问题上,由于foo()按值返回,因此编译器必然会产生一个临时对象,也就是说 auto_ptr<int> q(foo()); 这一句中q的构造函数传入的参数是一个右值,因此若想让这一句成功的调用,它的原型必须是这样的:auto_ptr(const auto_ptr&); 但是这样行吗?显然是不行的,因为我们还要剥夺原有指针对资源的拥有权呢,如果采用const引用,那是无法进行剥夺的,因为你无法修改它。你也许想到了另一个办法,我们只要用mutable修饰核心数据域的话,那么即使它是const也可以进行修改它的核心数据。这个办法看起来似乎不错,但如果我们在考虑一下下面这种情况,你或许会改变你的看法,假设有一个const auto_ptr ,如果我们把它赋值给另一个auto_ptr的话,你说应该是怎样的情况发生,呵呵,当然应该是禁止了,因为你不应该试图去改变一个const对象,即禁止剥夺一个const auto_ptr对资源的拥有权。但是如果按照你的想法,采用mutable的话,这种改变是可以实现的,因此你应该打消采用mutable的念头。那么难道就没有办法了吗?当然有,但是不太容易想到,请看下面这个简单的例子:

class X
{
private:
 int value;
public:
 X(int v=0)
 {
 value=v;
 }
 X(X& a)
 {
 value=a.value;
 a.value=0;
 }
 int set(int v)
 {
 value=v;
 return value;
 }
 friend ostream& operator << (ostream& os, const X& x)
 {
 os<<x.value<<endl;
 }
};

X f()
{
 X a(100);
 return a;
}

int main()
{
 X c(f());
 cout<<c;
 return 0;
}
上面这个例子和我们所遇到的情况有些相似X c(f());这一句是无法调用成功的,而且也不能把复制构造函数的引用参数变为const,因为我们要修改参数。Ok,我们就利用这个简单的例子来解决我们所遇到的问题。既然我们已经想到的一些方案不能达到我们的目的,那我们怎么做呢,对了,我们可以用类型转换函数。下面让我来帮你整理一下思路:
1.我们首先应该先定义一个类型转换层,它的核心数据应该和X的核心数据一样,例如:
struct Y
{
 int val;
 Y(int v):val(v){}
};
有了这个转换层,我们就可以先把函数f()返回时所生成的临时对象通过一个从X到Y的转换函数转型到Y。然后再通过一个从Y到X的转换函数进行对象的构造。至此,所有问题都得以解决。下面让我们来看一下具体方法。
2.添加一个从X到Y的类型转换函数。如下:
operator Y()
{
 Y y(value);
 return y;
}
3.添加一个从Y到X的类型转换函数,即只有一个参数的构造函数。

X(Y a)
{
 value=a.val;
}
OK,大功告成,你可以把这个例子在你的编译器上实现,果然能够解决所有的问题(在VC上会有一点问题,因为VC在临时对象这一点上对标准C++的支持不够好,用临时对象作参数的时候不加const也可以编译通过),下面我给出auto_ptr的一个实作范例,我想你应该能够理解它了:)

template<class Y>
struct auto_ptr_ref
{
 Y* yp;
 auto_ptr_ref(Y* rhs):yp(rhs){}
};  //注意这个转换层

template<class T>
class auto_ptr1
{
private:
 T* ap;
public:
 typedef T element_type;

 explicit auto_ptr1(T* ptr=0) throw():ap(ptr){}

 auto_ptr1(auto_ptr1& rhs) throw():ap(rhs.release()){}

 template<class Y>
 auto_ptr1(auto_ptr1<Y>& rhs) throw():ap(rhs.release()){}

 auto_ptr1& operator = (auto_ptr1& rhs) throw()
 {
   reset(rhs.release());
   return *this;
 }

 template<class Y>
 auto_ptr1& operator = (auto_ptr1<Y>& rhs) throw()
 {
   reset(rhs.release());
   return *this;
 }

 ~auto_ptr1() throw()
 {
   delete ap;
 }

 T* get() const throw()
 {
   return ap;
 }

 T& operator *() const throw()
 {
   return *ap;
 }

 T* operator ->() const throw()
 {
   return ap;
 }

 T* release() throw()
 {
   T* tmp(ap);
   ap=0;
   return tmp;
 }

  void reset(T* ptr=0) throw()
 {
 if(ap!=ptr)
 {
   delete ap;
   ap=ptr;
 }
 }

 auto_ptr1(auto_ptr_ref<T> rhs) throw():ap(rhs.yp){}

 auto_ptr1& operator = (auto_ptr_ref<T> rhs) throw()
 {
   reset(rhs.yp);
   return *this;
 }

  template<class Y>
 operator auto_ptr_ref<Y>() throw()
 {
   return auto_ptr_ref<Y>(release());
 }

 template<class Y>
 operator auto_ptr1<Y>() throw()
 {
   return auto_ptr1<Y>(release());
 }
};

好了,今天就说到这吧,大家有什么问题可以提出