C++进阶:智能指针之auto_ptr

534 阅读9分钟

概念

如今,垃圾回收机制已经大行其道,得到了诸多编程语言的支持,例如 Java、Python、C#、PHP 等。而 C++ 虽然从来没有公开地支持过垃圾回收机制,但 C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;而到了C++11 更是增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收,并且新标准废弃了不太好用的 auto_ptr。

熟悉C++的程序员都知道,在C++程序中 new 操作符的使用一定要与 delete 匹配,尽管匹配了,在某些场合仍然可能有内存溢出。比如当异常被抛出时,程序的正常控制流程被改变,可能导致潜在的内存溢出。而所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。 智能指针是行为虽然类似于指针的类对象,但这种对象还有一些其他功能。本节先介绍C++智能指针“鼻祖”——auto_ptr,它能基本实现我们所期望的功能。它在03版本中标准库第一次引入,它也是当时唯一的智能指针类型。

为什么要引入智能指针

最终的目的当然是为了更好的编程以及减少错误。auto_ptr是一个模板类,可以帮助管理动态内存分配的智能指针模板。先来看需要哪些功能以及这些功能是如何实现的。请看下面的函数:

void remodel(std::string* str) {
	std::string* ps = new std::string(str);
	str = *ps;
	return;
}

你可能已经看出了上面程序的缺陷。每当调用时,该函数都分配堆中的内存,但从不收回,从而导致内存泄漏。你可能也知道解决办法——在return语句前添加delete语句,以释放分配的内存即可;再看下面的函数:

void remodel(std::string* str) {
	std::string* ps = new std::string(str);
	
	if (true) {
		throw exeption();
	}
	str = *ps;
	delete ps;
	return;
}

当出现异常时,delete将不被执行,因此也将导致内存泄漏。我们来看一下如果想要更巧妙的解决上述问题需要些什么。当remodel( )函数退出时,函数内的局部变量将从栈内存中删除——因此指针ps变量占据的内存将被释放。如果ps指向的内存也被释放,那就完美了。这时,我们会想到C++经典的RAII机制(不认识的自行找度娘),那就是:如果ps有一个析构函数,该析构函数将在ps过期时释放它指向的内存。因此,ps的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果它是对象,则可以在对象过期时,让它的析构函数删除指向的内存。这正是我们这篇文章的主角——auto_ptr背后的思想,当然还有C++11新增加的unique_ptr和shared_ptr。模板auto_ptr是C++98提供的解决方案,C++11已将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了多年;最初的思想还很值得我们学习。 这三个智能指针模板(auto_ptr、unique_ptr和shared_ptr)都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当智能指针过期时,其析构函数将使用delete来释放内存。因此,如果将new返回的地址赋给这些对象,将无须记住稍后释放这些内存:在智能指针过期时,这些内存将自动被释放。智能指针模板定义在头文件<memory>中。

auto_ptr模板类

class auto_ptr {
    // 类型是模板参数 Type 的同义词
    typedef Type element_type;
    // 构造函数1, 以指针 ptr 构造 auto_ptr
    explicit auto_ptr(Type* ptr = 0) throw();
    // 构造函数2, 以 right 所保有的指针构造 auto_ptr , 调用 right.release() 获取该对象的所有权
    auto_ptr(auto_ptr<Type>& right) throw();
    // 构造函数3, 同2,Other*必须可以隐式转换为Type*
    template <class Other>
    auto_ptr(auto_ptr<Other>& right);
    // 构造函数4, 同2, 只不过right的类型从引用变成了专门的_ptr_ref,_ptr_ref可以接收右值,
    // 上面的2因为没有const,所以只能接收左值
    auto_ptr(auto _ptr_ref<Type> right) throw();
    // 将某种 auto_ptr 强制转换为另一种 auto_ptr
    template <class Other>
    operator auto_ptr<Other>() throw();
    template <class Other>
    auto_ptr<Type>& operator=(auto_ptr<Other>& right) throw();
	// 一个赋值运算符,用于将所有权从一个 auto_ptr 对象转移到到其他对象。
    auto_ptr<Type>& operator=(auto_ptr<Type>& right);
    ~auto_ptr();
    // auto_ptr 类型的对象的取消引用运算符,间接运算符返回 *get。 因此,存储的指针不能为空。
    Type& operator*() const throw();
    // 成员访问的运算符
    Type * operator->()const throw();
    // 该成员函数将返回存储的指针 myptr。
    Type *get() const throw();
		 // 释放被管理对象的所有权,返回保有的指针
    Type *release()throw();
	  // 替换ptr为保有的指针。若当前实现保有的指针不是空指针,则调用 delete get() 。
    void reset(Type* ptr = 0);
};

下面是简单的示例:

#include <memory>
#include <iostream>
#include <vector>

using namespace std;

class Int
{
public:
   Int(int i) {
      cout << "Constructing " << ( void* )this  << endl;
      x = i;
      bIsConstructed = true;
   };
   ~Int() {
      cout << "Destructing " << ( void* )this << endl;
      bIsConstructed = false;
   };
   Int &operator++() {
      x++;
      return *this;
   };
   int x;
private:
   bool bIsConstructed;
};

void function(auto_ptr<Int> &pi){
   ++( *pi );
   auto_ptr<Int> pi2( pi );
   ++( *pi2 );
   pi = pi2;
}
void function2(auto_ptr<Int> arg) {
}
int main( ) {
{
    auto_ptr<Int> pi (new Int(5));
    cout << pi->x << endl;
    function(pi);
    cout << pi->x << endl;
}
{
    auto_ptr<int> pi (new int(5));
	   // 显式类型转换
    auto_ptr<const int> pc = (auto_ptr<const int>)pi;
}
{
    const auto_ptr<Int> ciap(new Int(1));
    auto_ptr<Int> iap(new Int(2));

    // Error: this implies transfer of ownership of iap's pointer
    // function2(ciap);
    function2(iap); // compiles, but gives up ownership of pointer

    // here, iap owns a destroyed pointer so the following is bad:
    // *iap = 5; // BOOM

    cout << "main exiting\n";
}
// get/release/reset
{
    auto_ptr<Int> pi(new Int(5));
    pi.reset(new Int(6));
    Int* pi2 = pi.get();
    Int* pi3 = pi.release();
    if (pi2 == pi3)
        cout << "pi2 == pi3" << endl;
    delete pi3;
}
}

再看下面的示例2:

#include <iostream>
#include <memory>
using namespace std;
 
class Test {
public:
    Test(int num = 0) {
        num_ = num;
        cout << "Test:" << num_ << endl;
    }
    
    ~Test() {
        cout << "~Test:" << num_ << endl;
    }
 
    void Print() {
        cout << "Print:" << info_extend_.c_str() << endl;
    }
 
    int num_;             //代表这个对象的序号。
    string info_extend_;   //附加的字符串。
};

void TestAutoPtr() {
    auto_ptr<Test> pt (new Test(1));    //绑定一个Test类的新建对象
    if(pt.get()) {  //get函数用来显式返回它拥有的对象指针,此处判非空
        pt->Print();
        pt.get()->info_extend_ = "Addition"; //对类内成员进行操作
        pt->Print();                        //看是否成功操作
        (*pt).info_extend_ += " other";     //实验解引用操作
        pt->Print();
    }
}

int main( ) {
    TestAutoPtr();
}

输出:

Test:1
Print:
Print:Addition
Print:Addition other
~Test:1

为什么被废弃

问题1

我们知道共享智能指针是使用引用计数实现的,但auto_ptr的实现与引用计数型智能指针不同的,它要求对"裸"指针的完全占有性,类似unique_ptr。也就是一个"裸"指针不能同时被两个以上的auto_ptr所拥有。那么,在拷贝构造或赋值操作时,我们必须作特殊的处理来保证这个特性。 auto_ptr的做法是"所有权转移" ,即拷贝或赋值的源对象将失去对"裸"指针的所有权,所以,与一般拷贝构造函数,赋值函数不同,auto_ptr的拷贝构造函数,赋值函数的参数是引用(reference)而不是常引用(const reference)。当然,一个auto_ptr也不能同时拥有两个以上的"裸"指针,所以,拷贝或赋值的目标对象将先释放其原来所拥有的对象。

因为一个auto_ptr被拷贝或被赋值后,其已经失去对原对象的所有权, 这个时候,对这个auto_ptr的解引用(dereference)操作是不安全的。

void TestAutoPtr2() {
    auto_ptr<Test> pt1 (new Test(1));
    if(pt1.get()) {
        //先赋予一个初始值作为区别两个指针的标志
        (*pt1).info_extend_ = "Test";
        
        auto_ptr<Test> pt2;
        pt2 = pt1;
        pt2->Print();  //打印发现成功转移
        pt1->Print();  //崩溃,pt1对原指针的所有权已经被pt2所拥有,pt1不能再进行操作
    }
}

问题2

再看下面这个示例:

void Fun(auto_ptr <Test> ap) {
    *ap;
}
void TestAutoPtr3() {
    auto_ptr<Test> pt(new Test(1));
    Fun(pt);
    pt->Print();  //崩溃
}

我们看下上面的程序,这里问题比较隐蔽,很容易就出错了,所以auto_ptr作为函数参数按值传递是一定要避免的。可能你们会觉得用auto_ptr的指针或引用作为函数参数传递不久行了,但是再仔细想一下,引用或指针传递给函数时,而我们并不知道在函数中对传入的auto_ptr做了什么,如果当中某些操作使其失去了对原指针的所有权,那么这还是可能会导致致命的执行期错误。或许用const reference的形式来传递auto_ptr会是一个不错的选择。

那么为什么按值传递会引起问题呢,我们知道按值传递会产生一个临时对象来接受参数。到这里应该很熟悉了吧,这里出现了赋值运算符,pt赋值给一个临时变量,因为auto_ptr对赋值运算符重载的关系原指针就被置空了。只不过这次Fun函数结束后临时对象被析构了,pt也被置空了,原指针就被临时对象析构时delete掉了。解决的方法也很简单,Fun函数参数改为引用传递就可以了,最好是const哦。

问题3

我们再看下面这种用法:

int *pi = new int[10];
auto_ptr<int> api(pi);

如上所示,假如想要动态申请一个数组,释放时必须就得使用delete[],但是auto_ptr析构时删除指针用的是delete,而不是delete[],所以不能用auto_ptr来管理一个数组指针。

问题4

我们再看下面这种用法:

vector<auto_ptr<int>> v(10);

代码肯定是无法编译通过的。C++标准中提到:“STL元素必须具备拷贝构造和可赋值……”,意思就是说对象可以进行安全的赋值操作,可以将一个对象拷贝到另一个对象,从而获得两个独立的,逻辑上相同的拷贝。尤其是当一个对象被拷贝到目标对象后,原来的对象不会改变。但 auto_ptr 却不然,用 auto_ptr 进行赋值和拷贝操作不仅会改变目标拷贝,而且还明显地改变原来的对象。明确地说,就是原来对象将指针的物主身份转换成目标对象,与此同时,原来对象中的指针变成了NULL。下面是auto_ptr的拷贝构造函数,拷贝时确实对原对象调用了release,释放了原指针的所有权。

auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) { }

总结

使用auto_ptr的有很多注意事项:

  • auto_ptr 不能指向数组
  • auto_ptr 不能共享所有权
  • auto_ptr 不能通过复制操作来初始化
  • auto_ptr 不能放入容器中使用
  • auto_ptr 不能作为容器的成员
  • 不能把一个原生指针给两个智能指针对象管理

其实本篇文章介绍的auto_ptr虽然被废弃了,而且本身也有很多问题。但是从另一方面来说,只有了解了它的不足,才能更好了解它的替代者——unique_ptr和share_ptr,它们就是接下来几篇文章要介绍的主角。