C++2.0

智能指针学习笔记

2019-08-11  本文已影响0人  advanced_slowly

1. 介绍

本文介绍智能指针的使用。智能指针是c++ 中管理资源的一种方式,用智能指针管理资源,不必担心资源泄露,将c++ 程序员 从指针和内存管理中解脱出来,再者,这也是c++发展的趋势(这话不是我说的,见《Effective c++》和《c++实践编程》),应该认真学习一下。

智能指针中,最有名的应该数auto_ptr,该智能指针已经被纳入标准库,只需要包含<memory>头文件即可以使用,另外,TR1文档定义的shared_ptr和weak_ptr也已经实现(我用的gcc版本是gcc 4.6.1),它们的头文件是<tr1/memory> 。除此之外,还有几个好用的智能指针,不过它们属于boost库,不属于STL ,所以,用不用得到,根据自己的需要。不过,了解一下总无妨,正所谓”功不唐捐”嘛。

下面分别介绍auto_ptr,scoped_ptr,scoped_array,shared_ptr,shared_array, weak_ptr 和 intrusive_ptr 。

2. auto_ptr

2.1 为什么要用智能指针

在介绍第一个智能指针之前,先介绍下为什么要使用智能指针。先看下面这个函数:

void f()
{
    classA* ptr = new classA; // create an object explicitly
    ...                       // perform some operations
    delete ptr;               // clean up(destory the object explicitly)
}

这个函数是一系列麻烦的根源,一个显而易见的问题是,我们经常忘了delete 动作,特别是当函数中间存在return 语句时更是如此。然而,真正的麻烦发生于更隐晦之处,那就是当异常发生时,我们所要面对的灾难,异常一旦出现,函数将立刻退离(在函数内部delete语句前产生异常并抛出),根本不会调用函数尾端的delete 语句。结果可能是内存遗失(资源泄漏)。防止这种资源遗失的常见办法就是捕捉所有异常,例如:

void f()
{
    classA* ptr = new classA; // create an object explicitly
    try{
    ...                       // perform some operations
    }
    catch(...){
        delete ptr; //-clean up
        throw;//-rethrow the exception
    }

    delete ptr;               // clean up(destory the object explicitly)
}

你看,为了异常发生时处理对象的删除工作,程序代码变得多么复杂和累赘!如果还有第二个对象,如果还需要更多的catch 子句,那么简直是一场恶梦。这不是优良的编程风格,复杂而且容易出错,必须尽力避免。

如果使用智能指针,情况就会大不相同了。这个智能指针应该保证,无论在何种情形下,只要自己被摧毁,就一定要连带释放其所指资源。而由于智能型指针本身就是局部变量,所以无论是正常退出还是异常退出,只要函数退出,它就一定会被销毁。auto_ptr正是这种智能型指针。

2.2 auto_ptr

auto_ptr 是这样一种指针:它是”它所指向的对象”的拥有者,所以,当身为对象拥有者的auto_ptr 被摧毁时,该对象也将遭到摧毁。auto_ptr 要求一个对象只能有一个拥有者,严禁一物二主。更详细的说, auto_ptr 管理的资源必须绝对没有一个以上的auto_ptr 同时指向它。 这是因为资源的拥有者在销毁的时候,会销毁它所拥有的资源,资源不能释放两次,如果同时有两个auto_ptr拥有同一个资源,那么,在第一个auto_ptr销毁以后,第二个auto_ptr就成为野指针了,所以,任何时刻,一个资源只有一个拥有者。

下面是上例改写后的版本:

#include <iostream>
#include <memory>
using namespace std;

void f()
{
    //create and initialize an auto_ptr
    std::auto_ptr<classA> ptr(new classA);

    ... //perform some operations
}

不需要delete ,也不再需要catch 了。auto_ptr 的接口与一般指针非常相似:operator *用来提取其所指对象,operator-> 用来指向对象中的成员。然而,所有指针算法(包括++在内)都没有定义。

注意,auto_ptr< >允许你使用一般指针惯用的赋值(assign)初始化方式。(这里笔者认为是不允许,可能作者笔误)必须直接使用数值来完成除始化(意思就是显式初始化,不允许隐式初始化.该类中的构造函数是用explicit关键字声明的):

std::auto_ptr<classA> ptr1(new classA); //OK
std::auto_ptr<classA> ptr2 = new classA;//ERROR

有了上面两条语句,那么下面的问题就很显然了。

std::auto_ptr<classA> ptr;               // create an auto_ptr
ptr = new classA;                        // ERROR
ptr = std::auto_ptr<classA>(new classA); // ok, delete old object and own new

2.3 auto_ptr 拥有权的转移

上面提到过,绝对没有一个以上的auto_ptr同时指向同一个资源,那么,如果你复制(或赋值)一个auto_ptr指针会发生什么呢?发生拥有权转移,如下所示:

//initizlize an auto_ptr with a new object
std::auto_ptr<classA> ptr1(new classA);

//copy the auto_ptr
std::auto_ptr<classA> ptr2(ptr1);

在第一个语句中,ptr1拥有了那个new 出来的对象。在第二个语句中,拥有权由ptr1 转移到ptr2,此后,ptr2拥有那个对象,ptr1则是一个空指针。

同理,赋值动作也会发生拥有权的转移。

//initizlize an auto_ptr with a new object
std::auto_ptr<classA> ptr1(new classA);

//copy the auto_ptr
std::auto_ptr<classA> ptr2;
ptr2 = ptr1;

在上面的语句中,如果ptr2已经拥有一个对象,则,赋值动作发生时,会调用delete,将该对象删除。

因为auto_ptr 会发生拥有权转移问题,所以,不能完全像使用普通指针一样使用auto_ptr ,下面这个错误的用法演示的auto_ptr 的特性。

//this is a bad example
template <class T>
void bad_print(std::auto_ptr<T> p)//p gets ownership of passed argument
{
    //does p own an object?
    if (p.get() == NULL) 
    {
        std::cout << "NULL";
    }
    else
    {
        std::cout << *p;
    }
    //Oops,exiting delete the object to which p refers
}


int main(int argc, char const* argv[])
{
    std::auto_ptr<int> p(new int);
    *p = 42;      // change value to which p refers
    bad_print(p); // Oops,deletes the memory to which p refers
    *p = 18;      // RUNTIME ERROR
    return 0;
}

我们只是想通过print函数,打印对象的值,可是,却不小心把对象给销毁了,这是非常低级的错误,再多用几次auto_ptr 以后,就不会出现这种情况了。如果我们不是通过传值,而是通过传递一个引用会怎么样呢?可以这么做,可是,你得非常小心,千万别在调用的函数里面将资源的拥有权转移了。正确的用法应该声明指针常量,如下所示:

const std::auto_ptr<int>p(new int);
*p = 42       // change value to which p refers
bad_print(p); // COMPILE-TIME ERROR
*p = 18;      // OK

注意,auto_ptr 是一个指针,const auto_ptr 要表达的意思是”指针常量,指针不可指向其他资源,但是指针所指之物可以修改”,而不是指向常量的指针。所以,const auto_ptr 类似于 T* const p而不是指向常量的指针const T* p 。下面是一个使用auto_ptr 指针的完整示例:

#include <iostream>
#include <memory>
using namespace std;

//define output operator for auto_ptr
//-print object value or NULL

template<class T>
ostream& operator<< (ostream &strm, const auto_ptr<T> &p)
{
    //does p own an object ?
    if (p.get() == NULL) 
    {
        strm << "NULL";
    }
    else
    {
        strm << *p;
    }
    return strm;
}

int main(int argc, char* argv[])
{
    auto_ptr<int> p(new int(42));
    auto_ptr<int> q;

    cout << "after initizlization:" << endl;
    cout << "p : " << p << endl;
    cout << "q : " << q << endl;

    q = p;
    cout << "after assigning auto pointers:" << endl;
    cout << "p : " << p << endl;
    cout << "q : " << q << endl;

    *q += 13;
    p = q;
    cout << "after change and reassignment" << endl;
    cout << "p : " << p << endl;
    cout << "q : " << q << endl;
    return 0;
}

输出结果如下:

after initizlization:
p : 42
q : NULL
after assigning auto pointers:
p : NULL
q : 42
after change and reassignment
p : 55
q : NULL

2.4 auto_ptr 注意事项

auto_ptr(以及后面介绍的std::tr1::shared_ptr) 在其析构函数内做delete,而不是delete[]动作,那意味着在动态分配而得到的array上使用auto_ptr(或tr1::shared_ptr)是一个馊主意。但是,这样的代码是可以通过编译的,所以需要用户自己留心。下面的代码就会出现用new []分配资源,用delete而不是delete[] 释放资源一样的问题。

std::auto_ptr<std::string> aps(new std::string[10]);//资源泄漏
std::tr1::shared_ptr<int> spi(new int[1024]);       //资源泄漏

标准容器需要元素具有可复制和可赋值的特性,而复制和赋值操作会使auto_ptr 发生所有权转移,所以,auto_ptr 不能存放在容器中。

3. scoped_ptr

有了上面对auto_ptr 的解释,理解scoped_ptr 就没有什么难度了。scoped_ptr 的名字向读者传递了明确的信息,这个智能指针只能在本作用域中使用,不希望被转让。 scoped_ptr 通过将拷贝构造函数和operator= 函数声明为私有,以此阻止智能指针的复制,也就关闭了所有权转移的大门。

scoped_ptr 的用法与auto_ptr 几乎一样,大多数情况下它可以与auto_ptr 相互替换,它也可用从一个auto_ptr 获得指针的管理权(同时,auto_ptr 失去管理权)

scoped_ptr 也具有与auto_ptr 同样的”缺陷”——不能用作容器的元素,但原因不同,auto_ptr 是因为它的转移语义,而scoped_ptr 则是因为不支持拷贝和赋值,不符合容器对元素类型的基本要求。

下面的代码演示了scoped_ptr 与auto_ptr 的区别。

auto_ptr<int> ap(new int(10)); // 一个auto_ptr<int>
scoped_ptr<int> sp(ap);        // 从auto_ptr 获得原始指针
assert(ap.get() == 0);         // 原auto_ptr 不再拥有指针

ap.reset(new int(20));         // auto_ptr 拥有新的指针
cout << *ap << "," << *sp << endl;

auto_ptr<int> ap2;
ap2 = ap;              // ap2 从ap 获得原始指针,发生所有权转移
assert(ap.get() == 0); // ap 不再拥有指针
scoped_ptr<int> sp2;   // 另一个scoped_ptr
sp2 = sp;              // 赋值操作,无法通过编译

比起auto_ptr ,scoped_ptr 更明确的表达了代码原始编写者的意图:只能在定义的作用域内使用,不可转让。

4. scoped_array

scoped_array 与scoped_ptr 没什么区别,主要区别就是用 new[] 分配资源,用 delete [] 释放资源,而scoped_ptr 用new 分配资源,用delete 释放资源。用法如下:

#include <iostream>
#include <algorithm>
#include <iterator>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

int main(int argc, char* argv[])
{
    int *arr = new int[100]; //动态分配资源
    scoped_array<int> sa(arr);//用scoped_array 对象代理原始动态数组
    //scoped_array<int> sa( new int[100]);

    fill_n(&sa[0], 100, 5);
    sa[10] = sa[20] + sa[30];//像普通数组一样使用

    cout << sa[10] << "\t" << sa[20] << endl;
    return 0;//在作用域最后,自动释放资源
}

scoped_array 与 scoped_ptr 接口和功能几乎一样,主要区别如下:
1.构造函数接受的指针p 必须是new [] 的结果,而不能是new 表达式的结果。
2.没有* , -> 操作符重载,因为scoped_array 持有的不是一个普通指针
3.析构函数使用delete []释放资源,而不是delete
4.提供operator[] 操作符重载,可以像普通数组一样使用下标访问元素
5.没有begin() end() 等类似容器的迭代器

上面这个例子,可以很方便的使用vector代替,《Boost 程序库开发指南》的作者并不推荐使用scoped_array。

5. shared_ptr

5.1 shared_ptr 介绍

上面已经介绍了3种智能指针,如果按照重要程度排序,auto_ptr 是最重要的,其次应该算shared_ptr 了,shared_ptr 已经被纳入标准库了,用gcc 的用户只需要#include<tr1/memory>用visual studio 08/10 的用户通过加入头文件#include<memory>即可。

shared_ptr 是一个最像指针的”智能指针”,它实现了引用计数的功能,所以,指针可以随意复制,在函数间传递,或者存储在容器里面。

shared_ptr 还有两个特有的成员函数,分别是:
1.unique() 用于检查指针是否唯一的,如果是唯一的,就相当于auto_ptr
2.use_count() 返回当前指针的引用计数,use_count() 不提供高效率的操作,所以,use_count() 应该仅仅用于测试或者调试。

下面看看shared_ptr 的用法:

#include <iostream>
#include <tr1/memory>
#include <assert.h>
using namespace std;

int main(int argc, char* argv[])
{
    std::tr1::shared_ptr<int> sp( new int(10));//一个指向整数的shared_ptr
    assert( sp.unique());//现在shared_ptr 是指针的唯一持有者

    std::tr1::shared_ptr<int> sp2 = sp;//第二个shared_ptr ,拷贝构造函数

    //两个shared_ptr 相等,指向同一个对象,且引用计数为2
    assert(sp == sp2 && sp.use_count() == 2);

    *sp2 = 100;//使用解引用操作符修改被指对象
    assert(*sp == 100);//另一个shared_ptr 同时也被修改

    sp.reset();
    assert(!sp);//sp 不再持有对象
    return 0;
}

再看一个复杂一点的例子,用以演示智能指针作为成员变量和函数参数的情况。

#include <iostream>
#include <tr1/memory>
#include <assert.h>
using namespace std;
using namespace std::tr1;

class shared{
    private:
        shared_ptr<int> p;
    public:
    shared(shared_ptr<int> p_):p(p_){}; 
    void print()
    {
        cout << "count:" << p.use_count() << " v = " << *p << endl;
    }
};

void print_fun(shared_ptr<int> p)
{
        cout << "count:" << p.use_count() << " v = " << *p << endl;
}

int main(int argc, char* argv[])
{
    shared_ptr<int> p(new int(100));
    shared s1(p), s2(p);

    s1.print();
    s2.print();

    *p = 20;
    print_fun(p);
    s1.print();
}

输出结果如下:

count:3 v = 100
count:3 v = 100
count:4 v = 20
count:3 v = 20

可以看到,我们不用关心shared_ptr 的具体实现,也不需要烦心它的引用计数是多少,我们只需要把它当成一个普通指针使用,再也不用担心资源泄漏。

auto_ptr 不能一物侍二主,所以,拷贝的时候会发生所有权转移,而shared_ptr 则不存在这个问题呢,那么,把一个 auto_ptr 复制给 shared_ptr 或者把一个shared_ptr 复制给auto_ptr 会发生什么呢?答案是编译错误,即你不能这么做。

5.2 make_shared

前面说过,shared_ptr 是最像指针的智能指针,有了shared_ptr ,我们几乎可以抛弃delete了,但是,我们还是用到了new,用到了new 而不delete ,很不对称不是吗,所以,TR1又定义了一个小工具make_shared(类似与make_pair)来帮助我们生成对象,不过,这个功能好像还没有实现,如果,等不及要玩一下,可以用boost库,make_shared 在头文件#include<boost/make_shared.hpp>中定义,使用方法如下:

#include <iostream>
#include <string>
#include <vector>
#include <boost/make_shared.hpp>
using namespace std;

int main(int argc, char const* argv[])
{
    boost::shared_ptr<string> sp = boost::make_shared<string>("make_shared");
    cout << *sp << endl;

    boost::shared_ptr< vector<int> > spv = boost::make_shared< vector<int> >(10, 2);
    cout << spv->size() << endl;
    return 0;
}

shared_ptr 可以应用于标注库,唯一需要牢记的是,shared_ptr 是一个指针,行为类似于普通指针,知道这一点以后,下面的代码就不难理解了。

#include <boost/make_shared.hpp>
#include <iostream>
#include <vector>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;

int main(int argc, char const* argv[])
{
    typedef vector< shared_ptr<int> > vs;
    //声明一个持有shared_ptr 的标准容器类型,元素被初始化为空指针
    vs v(10);

    int i = 0;
    for (vs::iterator pos = v.begin(); pos != v.end(); ++pos) 
    {
        (*pos) = make_shared<int>(++i);//使用工厂函数(make_shared)赋值
        cout << *(*pos) << ", ";//输出值
    }
    cout << endl;

    shared_ptr<int> p = v[9];

    *p = 100;
    cout << *v[9] << endl;
    return 0;
}

5.3 shared_ptr 的缺陷(循环引用)

shared_ptr 需要当心循环引用的问题,不然还是会发生资源泄漏。详细信息见这里。

6. shared_array

我们知道scoped_ptr 和 scoped_array 的用法和区别以后,很容易猜到shared_array 的用法了。

shared_array 类似于shared_ptr ,它包装了new[] 操作符在堆上分配的动态数组,同样,使用引用计数机制为动态数组提供了一个代理,可以在程序的生命周期里上期存在,直到没有任何引用后才释放内存。

shared_array 的接口和功能几乎与shared_ptr 是相同的,主要区别如下:
1.构造函数接受指针p必须是new[] 的结果,而不是new 分配的资源
2.提供operator[] 操作符重载,可以像普通数组一样用下标访问
3.没有* -> 操作符重载,因为shared_array 持有的不是一个普通指针
4.析构函数使用delete[] 释放资源,而不是delete

shared_array 用法的简单示例:

#include <iostream>
#include <boost/smart_ptr.hpp>
#include <assert.h>
using namespace std;
using namespace boost;

int main(int argc, char const* argv[])
{
    shared_array<int> sa( new int[100]);
    shared_array<int> sa2 = sa;

    sa[0] = 10;

    cout << sa.use_count() << endl;
    cout << sa[0] << endl;

    assert( sa2[0] == 10);
    return 0;
}

7. weak_ptr

关于weak_ptr 我是知其然,但不知其所以然。下面的说明和例子都来自《Boost 程序库完全开发指南》,无任何更改,没有理解透彻,怕改错了。

weak_ptr 被设计为与shared_ptr 共同工作,可以从一个shared_ptr 或者另一个weak_ptr 对象构造,获得资源的观测权。但是weak_ptr 没有共享资源,它的构造函数不会引起指针引用计数的增加。同样,在weak_ptr 析构时,也不会导致引用计数减少,它只是一个静静的观察者。

使用weak_ptr 的成员函数use_count() 可以观测资源的引用计数,另一个成员函数expired() 的功能等价于use_count() == 0,但更快,表示被观测的资源已经不复存在。

weak_ptr 没有重载operator* 和 -> ,这是特意的,因为它不共享指针,不能操作资源,这正是它”弱”的原因,但它可以使用一个非常重要的成员函数lock() 从被观测的shared_ptr 获得一个可用的shared_ptr 对象,从而操作资源。但当expired() == ture的时候,lock()函数返回一个存储空指针的shared_ptr 。

下面的代码示范了weak_ptr 的用法:

shared_ptr<int> sp (new int(10)); // 一个shared_ptr
assert(sp.use_count() == 1);

weak_ptr<int> wp(sp);        // 从shared_ptr 创建weak_ptr
assert(sp.use_count() == 1); // weak_ptr 不影响引用计数
if (!wp.expired())           // 判断weak_ptr 观察的对象是否有效
{
    shared_ptr<int> sp2 = wp.lock();//获得一个shared_ptr
    *sp2 = 100;
    assert(sp.use_count() == 2);
}//退出作用域,sp2 自动析构,引用计数减1

assert(sp.use_count() == 1);

sp.reset();           // shared_ptr 失效
assert(wp.expired()); // weak_ptr 将获得一个空指针
assert(!wp.lock());

8. intrusive_ptr

Boost 库实现了该指针,Boost 库不推荐使用intrusive_ptr。

9. 注意事项


在资源管理类中提供对原始资源的访问(Item 15)

使用智能指针的时候,可以通过get()成员函数,获取原始指针,从而与一下需要用到原始资源的API打交道,如果这样的API特别多,每次都写.get() 不光费时,而且不够清晰,这时,应该提供隐式类型转换(即类型转换函数)。如下所示:

  class Font{
      public:
          ....
          //隐式类型转换
          operator FondHandle() const
          {return f;}
          ...
  };


以独立的语句将newd 对象置入智能指针(Item 17)

应该用独立的语句将newd 的对象置入智能指针,考虑如下调用:

processWidget(std::tr1::shared_ptr(new Widget), priority());

在上面的调用中,需要处理以下三件事:
1.调用priority
2.执行new Widget
3.调用tr1::shared_ptr构造函数

c++ 编译器会以什么样的次序完成上面三件事,我们不得而知,如果调用序列如下:
1.执行new Widget
2.调用priority
3.调用tr1::shared_ptr构造函数

万一对priority 的调用导致异常,则new Widget返回的指针就会遗失,我们无法使用,也无法释放该资源,所以,安全的处理方式,应该是这样的:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

10. 总结

在上面所有介绍的智能指针中,auto_ptr ,shared_ptr 和weak_ptr 已经纳入标准库,可以放心使用,而不用担心可移植性的问题。其中auto_ptr 和shared_ptr 最为重要,shared_ptr和普通指针最为相似,不知道该用哪种类型的智能指针的时候,就用shared_ptr 。

参考资料

•《C++ 标准程序库》
•《Boost 程序库完全开发指南》
•《Effective c++》

上面内容转载自http://mingxinglai.com/cn/2013/01/smart-ptr/。十分感谢作者给我们带来的美文,让我对智能指针有了更深的巩固和和全面的复习。“书中自有颜如玉”,上面的内容到处可见侯捷老师的《Effective C++》的影子。因为这的确是一本好书。有机会也品读下罗剑锋的《Boost程序库完全开发指南》吧

谈谈个人对智能指针的补充:

1.unique_ptr

讲unique_ptr智能指针以前我们来讲讲auto_ptr.**auto_ptr是一种提供了严格的所有权语义的智能指针。一个智能指针对象拥有其内部指针所指对象的所有权,将一个auto_ptr对象拷贝或者赋值给另一个auto_ptr智能指针对象,将发生所有权的转移。因此使用已经转移了所有权的智能指针对象会发生不明确的行为(在运行期中断程序)。auto_ptr满足不了标准库容器可以拷贝和赋值的需求。所以auto_ptr这种智能指针已经被C++标准库deprecated。
auto_ptr源码:

#include <iostream>
#include <string>
#include <cassert>

template<typename _Tp1>
struct auto_ptr_ref
{
    //内部指针
    _Tp1* _M_ptr;
    //有参构造函数
    explicit auto_ptr_ref(_Tp1* __p): _M_ptr(__p) { }
};

template<typename _Tp>
class auto_ptr
{
private:
    _Tp* _M_ptr;

public:
    //指针指向的类型
    typedef _Tp element_type;

    explicit auto_ptr(element_type* __p = 0) throw() : _M_ptr(__p) { }

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

    //模板成员函数(泛型方法),_Tp1类型的指针必须能够转化为_Tp类型的指针
    template<typename _Tp1>
    auto_ptr(auto_ptr<_Tp1>& __a) throw() : _M_ptr(__a.release()) { }


    auto_ptr& operator=(auto_ptr& __a) throw()
    {
        reset(__a.release());
        return *this;
    }

    //模板成员函数,_Tp1类型的指针必须能够转化为_Tp类型的指针
    template<typename _Tp1>
    auto_ptr& operator=(auto_ptr<_Tp1>& __a) throw()
    {
        reset(__a.release());
        return *this;
    }

    ~auto_ptr() { delete _M_ptr; }

    //解除引用
    element_type& operator*() const throw()
    {
        //如果内部指针为空,则中止程序运行
        assert(_M_ptr != nullptr);
        return *_M_ptr;
    }

    //解除引用
    element_type* operator->() const throw()
    {
        assert(_M_ptr != nullptr);
        return _M_ptr;
    }

    //获取原始指针资源
    element_type* get() const throw() { return _M_ptr; }

    //将auto_ptr对象管理权转移
    element_type* release() throw()
    {
        element_type* __tmp = _M_ptr;
        _M_ptr = nullptr;
        return __tmp;
    }

    //强制重置_M_ptr,如果此函数不给参数则析构资源,否则将_p赋值给_M_ptr
    void reset(element_type* __p = 0) throw()
    {
        if (__p != _M_ptr)
        {
            delete _M_ptr;
            _M_ptr = __p;
        }
    }

    //自动转化
    auto_ptr(auto_ptr_ref<element_type> __ref) throw()
    : _M_ptr(__ref._M_ptr) { }

    //将__ref的所有权转让给_M_ptr
    auto_ptr& operator=(auto_ptr_ref<element_type> __ref) throw()
    {
        if (__ref._M_ptr != this->get())
        {
            delete _M_ptr;
            _M_ptr = __ref._M_ptr;
        }
        return *this;
    }

    //类型转化函数
    template<typename _Tp1>
    operator auto_ptr_ref<_Tp1>() throw()
    { return auto_ptr_ref<_Tp1>(this->release()); }

    //类型转换函数,将_Tp类型的指针对象_M_ptr转化为auto_ptr<_Tp1>
    template<typename _Tp1>
    operator auto_ptr<_Tp1>() throw()
    { return auto_ptr<_Tp1>(this->release()); }
};

//模板特化版本
template<>
class auto_ptr<void>
{
public:
    typedef void element_type;
};

对->,*,get()三个主要的函数测试如下:

class Student
{
private:
    std::string _name;
    int _age;

public:
    Student(const std::string& my_name,int my_age):_name(my_name),_age(my_age)
    {

    }
    Student()
    {
        _age = -1;
    }

    inline void showMess()const
    {
        std::cout << "name:" << _name << ",age:" << _age << std::endl;
    }

};

int main()
{
    auto_ptr<Student>stu1(new Student("xiao hua",19));
    stu1->showMess();       //->返回内部的指针
    (*stu1).showMess();     //.返回内部指针所指向的对象
    stu1.get()->showMess(); //get()返回内部的指针,不得修改指针所指向的资源

    auto_ptr<Student>stu2;
    stu2 = stu1;

    stu2->showMess();
    (*stu2).showMess();
    stu2.get()->showMess();

    //下面三句代码出错,调用assert运行期中止程序
    stu1->showMess();
    (*stu1).showMess();
    stu1.get()->showMess();

    return 0;
}

从上面这个简单的例子也可以看到,我在编译的时候是不出问题的,但是在运行期的时候突然中止运行程序,这对于开发程序是很危险的。能不能在编译期间就让编译器报这种引用空指针的错误呢?(原所有权者在赋值的过程中转移了所有权,置原智能指针的内部指针为空)设计标准库的大牛们呢就对auto_ptr做了改进,有了现在的unique_ptr,弃用了原来的auto_ptr。
unique_ptr删除了一些从左值拷贝构造,赋值等函数,但新增了移动拷贝构造,移动赋值版本的函数,当然增加的远不止这些。如下:

      // Disable copy from lvalue.
      unique_ptr(const unique_ptr&) = delete;
      unique_ptr& operator=(const unique_ptr&) = delete;

      // Disable construction from convertible pointer types.
      template<typename _Up, typename = _Require<is_pointer<pointer>,
           is_convertible<_Up*, pointer>, __is_derived_Tp<_Up>>>
    unique_ptr(_Up*, typename
           conditional<is_reference<deleter_type>::value,
           deleter_type, const deleter_type&>::type) = delete;

      // Disable construction from convertible pointer types.
      template<typename _Up, typename = _Require<is_pointer<pointer>,
           is_convertible<_Up*, pointer>, __is_derived_Tp<_Up>>>
    unique_ptr(_Up*, typename
           remove_reference<deleter_type>::type&&) = delete;

unique_ptr的内部源码比auto_ptr复杂了许多,因为它新增了C++11的很多东西,庞大了许多。关于unique_ptr可谓真的"一山容不得二虎",一物一主,也就是说一个堆上分配内存的对象只能有一个智能指针对象管理它,该智能指针的所有权是唯一的。如果有人试图转移它则编译器会报错,这正是程序员们希望的。比如下面这段代码:

#include <iostream>
#include <memory>

class Student
{
private:
    std::string _name;
    int _age;

public:
    Student(const std::string& my_name,int my_age):_name(my_name),_age(my_age)
    {

    }
    Student()
    {
        _age = -1;
    }

    inline void showMess()const
    {
        std::cout << "name:" << _name << ",age:" << _age << std::endl;
    }

};

int main()
{

    std::unique_ptr<Student>stu1(new Student("xiao hua",19));
    stu1.get()->showMess();
    stu1->showMess();
    (*stu1).showMess();

    //std::unique_ptr<Student>stu2(stu1);//compile-time error,use of delete function
    //stu2 = stu1;                       //compile-time error,use of delete function

    std::unique_ptr<Student>stu2;

    stu2 = std::move(stu1); //此时已将stu2的资源偷走,后面不能使用stu1
    stu2->showMess();

    //stu1->showMess();       //run-time error  
    //unique_ptr管理数组            
    std::unique_ptr<Student[]>stu3(new Student[3]);
    
    return 0;
}

unique_ptr类模板内部是有两个版本的,如下:

//unique_ptr for single object
template <typename _Tp, typename _Dp = default_delete<_Tp> >
class unique_ptr;
//unique_ptr for array objects
//模板的偏特化
template<typename _Tp, typename _Dp>
class unique_ptr<_Tp[], _Dp>;

也就是说,unique_ptr智能指针对象可以管理通过new表达式动态获得的单一对象,也可以管理通过new[]动态获得的数组。当然了unique_ptr既然支持管理动态分配的数组,其内部也就有重载[]函数,用于提供下标访问数组。前面提到过shared_ptr类模板中可以使用make_shared()模板函数获得shared_ptr类对象。在unique_ptr类中也是可以通过make_unique获得unique_ptr类对象的。

当然了,对于在创建通过new表达式获得的单一对象或者通过new[]获得的数组的unique_ptr智能指针对象时,可以自定义删除器,也可以不定义(有一个默认的删除器)。

现对该类做个简单的测试,下面的demo来自文档:

#include <iostream>
#include <vector>
#include <memory>
#include <cstdio>
#include <fstream>
#include <cassert>
#include <functional>

struct B {
  virtual void bar() { std::cout << "B::bar\n"; }
  virtual ~B() = default;
};
struct D : B
{
    D() { std::cout << "D::D\n";  }
    ~D() { std::cout << "D::~D\n";  }
    void bar() override { std::cout << "D::bar\n";  }
};

// a function consuming a unique_ptr can take it by value or by rvalue reference
std::unique_ptr<D> pass_through(std::unique_ptr<D> p)
{
    p->bar();
    return p;
}

void close_file(std::FILE* fp) { std::fclose(fp); }

int main()
{
  std::cout << "unique ownership semantics demo\n";
  {
      auto p = std::make_unique<D>(); // p is a unique_ptr that owns a D
      auto q = pass_through(std::move(p));
      assert(!p); // now p owns nothing and holds a null pointer
      q->bar();   // and q owns the D object
  }

  std::cout << "Runtime polymorphism demo\n";
  {
    std::unique_ptr<B> p = std::make_unique<D>(); // p is a unique_ptr that owns a D
                                                  // as a pointer to base
    p->bar(); // virtual dispatch

    std::vector<std::unique_ptr<B>> v;  // unique_ptr can be stored in a container
    v.push_back(std::make_unique<D>());
    v.push_back(std::move(p));
    v.emplace_back(new D);
    for(auto& p: v) p->bar(); // virtual dispatch
  } 
  

  std::cout << "Custom deleter demo\n";
  std::ofstream("demo.txt") << 'x'; // prepare the file to read
  {
      std::unique_ptr<std::FILE, decltype(&close_file)> fp(std::fopen("demo.txt", "r"),
                                                           &close_file);
      if(fp) // fopen could have failed; in which case fp holds a null pointer
        std::cout << (char)std::fgetc(fp.get()) << '\n';
  } // fclose() called here, but only if FILE* is not a null pointer
    // (that is, if fopen succeeded)

  std::cout << "Custom lambda-expression deleter demo\n";
  {
    std::unique_ptr<D, std::function<void(D*)>> p(new D, [](D* ptr)
        {
            std::cout << "destroying from a custom deleter...\n";
            delete ptr;
        });  // p owns D
    p->bar();
  } // the lambda above is called and D is destroyed

  std::cout << "Array form of unique_ptr demo\n";
  {
      std::unique_ptr<D[]> p{new D[3]};
  } // calls ~D 3 times
}

运行结果如下:
unique ownership semantics demo
D::D
D::bar
D::bar
D::~D
Runtime polymorphism demo
D::D
D::bar
D::D
D::D
D::bar
D::bar
D::bar
D::~D
D::~D
D::~D
Custom deleter demo
x
Custom lambda-expression deleter demo
D::D
D::bar
destroying from a custom deleter...
D::~D
Array form of unique_ptr demo
D::D
D::D
D::D
D::~D
D::~D
D::~D

如果想要理解的透彻还是耐着性子读源码吧。

上一篇下一篇

猜你喜欢

热点阅读