由一份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()); } };
好了,今天就说到这吧,大家有什么问题可以提出 
|