C++中的智能指针

1. 从auto_ptr说起

先来一段简单的代码,看看auto_ptr的使用:

/************************************************************************
> File Name: auto_ptrDemo.cpp
> Author:Jelly
> Mail:vipygd#126.com(#=>@)
> Created Time: 2014年10月15日 星期三 12时00分33秒
************************************************************************/
#include <iostream>          
#include <memory>            
using namespace std;         

class A                      
{                            
public:                      
    A()                      
    {                        
        cout<<"Construct A Object."<<endl;
    }                        

    ~A()                     
    {                        
        cout<<"Destroy A Object."<<endl;
    }                        
};                           

int main()                   
{                            
    auto_ptr<int> pInt(new int(10));
    cout<<*pInt<<endl;       

    auto_ptr<A> pAObj(new A);

    return 0;                
}

 哦,被你发现了;auto_ptr的初衷是用来实现智能指针的,实现内存的自动回收。比如,代码中我new了一个A对象,但是却没有对应的delete A对象。嗯,这得解决多少麻烦事啊,真的是好东西,而且还这么好用。好用?如果好用,我就不用写这篇博文来进行总结了。说道智能指针,就不得不对它的实现原理简单说说。

2. 智能指针

智能指针,无非就是进行垃圾回收,主要可以分为以下两大类:

    1. 基于引用计数的垃圾回收器。简单的说,引用计数主要是使用系统记录对象被引用的次数。当对象被引用的次数变为0时,该对象即可被视为“垃圾”,从而可以被回收。使用引用计数做垃圾回收的算法的一个优点是实现很简单,与其它垃圾回收算法相比,该方法不会造成程序暂停,因为计数的增减与对象的使用时紧密结合的。 具体的引用实例可以参见: 
      1. 智能指针-引用计数实现
      2. COM中的引用计数1
      3. COM中的引用计数2;
    2. 基于跟踪处理的垃圾回收机制。相比于引用计数,跟踪处理的垃圾回收机制被更为广泛地应用。其基本方法是产生跟踪对象的关系图,然后进行垃圾回收。使用跟踪方式的垃圾回收算法主要有以下几种:
      • 标记-清除(Mark-Sweep)
        这个算法可以分为两个过程。首先该算法将程序中正在使用的对象视为“根对象”,从根对象开始查找它们所引用的堆空间,并在这些堆空间上做标记。当标记结束后,所有被标记的对象就是可达对象或活对象,而没有被标记的对象就被认为是垃圾,在第二步的清扫阶段会被回收掉;
        这种方法的特点是活的对象不会被移动,但是其存在会出现大量的内存碎片问题。
      • 标记-整理(Mark-Compact)
        这个算法标记的方法和标记-清除方法一样,但是标记完之后,不再遍历所有对象清扫垃圾了,而是将活跃的对象向“左”靠齐,这就解决了内存碎片的问题。
        标记-整理算法有个特点就是移动活的对象,因此相应的,程序中所有对堆内存的引用都要更新。
      • 标记-拷贝(Mark-Copy)
        这种算法的一大特点就是将堆空间分为两部分:From,To。开始的时候我们只在From里分配,当From分配满的时候出发垃圾收集,这个时候会找出From空间里所有的存活对象,然后将这些存活的对象拷贝到To空间里。这样From空间里剩下的就都全是垃圾,而且对象拷贝到To里,在To里是紧凑排列的。这个事儿做完了之后From和To的角色就转变了一下。原来的From变成了To,原来的To变成了现在的From。现在又可以在这个完全是空的From里分配了。这个算法实现起来也很简单,高效(Sun JVM的新生代的垃圾回收就使用了这种算法)。不过这个算法有一个问题,堆的利用率只有一半了,这对那些内存占用率比较低的对象还算好,如果随着应用的内存占用率的增高,问题就出现了,第一个要拷贝的对象太多了,还有可能无法回收内存了。程序失败了。

3. 为什么auto_ptr难用?

总结了一点理论性的东西,我是最讨厌理论的,但是没有办法,你越讨厌的东西,你还越要去看,无奈。话题再收回来,上面也说了,auto_ptr不好用,为什么说auto_ptr不好用呢?在继续阅读下面的内容之前,我建议阅读一下这篇文章:《C++中的RAII机制》。 是的,auto_ptr并没有使用上面介绍的几种垃圾回收技术中的任何一种技术,而是使用的一种叫做RAII的机制实现的,所以,auto_ptr本质上一点都不智能。来看下面这段代码:

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

class A
{
public:
    A() { cout << "Construct A Object." << endl; }
    ~A() { cout << "Destroy A Object." << endl; }

    void SetA(int value) { m_a = value; }
    int GetA() { return m_a; }

private:
    int m_a;
};

void TestFunc(auto_ptr<A> Obj)
{
    Obj->SetA(20);
    cout << Obj->GetA() << endl;
}

int main()
{
    auto_ptr<A> pAObj(new A());
    //auto_ptr<A> pAObj1 = new A(); **This is wrong expression.

    pAObj->SetA(10);
    cout << pAObj->GetA() << endl;

    TestFunc(pAObj);

    //cout << pAObj->GetA() << endl; ** This is wrong.

    return 0;
}

看看,发生了什么。调用完TestFunc(pAobj)之后,我再调用pAObj->GetA()居然出错了。这就是auto_ptr的奇葩之处。为什么?在《C++中的RAII机制》中的使用RAII的陷阱一节有仔细的分析。

4. auto_ptr的庐山真面目

总是在说,总觉的缺点什么。看看auto_ptr的源代码吧(代码来源:SGI STL)。

template <class _Tp> class auto_ptr {
private:
    _Tp* _M_ptr; //实际wrap的指针

public:
    typedef _Tp element_type;

    // 显式构造函数,防止auto_ptr<A> pAObj1 = new A();隐式构造
    explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {}

    // 复制构造函数,知道为什么参数是&吗?
    auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
    template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
        : _M_ptr(__a.release()) {}

    // 赋值构造函数
    auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
        if (&__a != this) {
            delete _M_ptr;
            _M_ptr = __a.release();
        }
        return *this;
    }

    template <class _Tp1>
    auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
        if (__a.get() != this->get()) {
            delete _M_ptr;
            _M_ptr = __a.release();
        }
        return *this;
    }

    ~auto_ptr() { delete _M_ptr; }

    // 智能指针一般都要重载"*"和"->"操作符
    _Tp& operator*() const __STL_NOTHROW {
        return *_M_ptr;
    }
    _Tp* operator->() const __STL_NOTHROW {
        return _M_ptr;
    }
    _Tp* get() const __STL_NOTHROW {
        return _M_ptr;
    }
    _Tp* release() __STL_NOTHROW {
        _Tp* __tmp = _M_ptr;
        _M_ptr = 0;
        return __tmp;
    }
    void reset(_Tp* __p = 0) __STL_NOTHROW {
        if (__p != _M_ptr) {
            delete _M_ptr;
            _M_ptr = __p;
        }
    }
};

对于上面的代码,你有什么要问的么?阅读代码就会发现,auto_ptr不能共享内存,在同一时间,只有一个auto_ptr指向一个指定的内存。所以说,以下这种代码就会ptr2失效了。

auto_ptr<int> ptr(p);
auto_ptr<int> ptr2(p);
ptr = ptr2;

简直无法想象,一个赋值操作,导致右值失效了,怎么可以,这种问题,在实际开发中,我们不得不去注意。

5. 我们要注意什么?

是的,auto_ptr有不少坑,不可能列全了,但话又说回来了,以后基本上也会告别auto_ptr了,让我们再和auto_ptr愉快的玩最后一段时光吧。 当你、我,还有他在使用auto_ptr时,需要注意以下几点:

  • auto_ptr不能共享所有权;
  • auto_ptr不能指向数组(否则会造成内存泄漏问题);对于这点,我认为如果数组中存放的是POD类型时,或者含有trivial destructor时,是可以指向数组的;
  • auto_ptr不能作为容器的成员;C++标准已经明确禁止这么做了,否则可能会遇到不可预见的问题(STL容器在分配内存的时候,必须要能够拷贝构造容器的元素。而且拷贝构造的时候,不能修改原来元素的值。而auto_ptr在拷贝构造的时候,一定会修改元素的值。所以STL元素不能使用auto_ptr。)。

6. C++11中的智能指针

但是随着C++11的到来,auto_ptr已经不再了,即将成为历史;好的东西总是会受到大家的欢迎的,随着大家都在使用“准”标准库boost中的shared_ptr;C++标准委员会终于觉的是时候将shared_ptr加入到C++11中去了。欢呼声一片,至少我是这么觉的了;至少shared_ptr让我用起来,还是不错的。接下来,就总结一下C++11中的这些智能指针吧。

先来一段简单的代码,看看C++11中到底有哪些智能指针。

/*************************************************************************
> File Name: SmartPointDemo.cpp
> Author: Jelly
> Mail: vipygd#126.com(#->@)
> Created Time: 2014年10月16日 星期四 15时25分43秒
************************************************************************/

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

int main()
{    
    unique_ptr<int> up1(new int(10)); // 不能复制的unique_ptr
    // unique_ptr<int> up2 = up1; // 这样是错的
    cout<<*up1<<endl;

    unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针

    cout<<*up3<<endl;
    // cout<<*up1<<endl; // 运行时错误

    up3.reset(); // 显式释放内存
    up1.reset(); // 即使up1没有拥有任何内存,但是这样调用也没有问题
    // cout<<*up3<<endl; // 已经释放掉up3了,这样会运行时错误

    shared_ptr<int> sp1(new int(20));
    shared_ptr<int> sp2 = sp1; // 这是完全可以的,增加引用计数

    cout<<*sp1<<endl;
    cout<<*sp2<<endl;

    sp1.reset(); // 减少引用计数
    cout<<*sp2<<endl;

    return 0;
}

C++11中主要提供了unique_ptrshared_ptrweak_ptr这三个智能指针来自动回收堆分配的对象。看看上面的代码,感觉用起来也还挺轻松的,也还不错,至少是比auto_ptr好点。

7. unique_ptr

C++11中的unique_ptrauto_ptr的替代品,它与auto_ptr一样拥有唯一拥有权的特性,与auto_ptr不一样的是,unique_ptr是没有复制构造函数的,这就防止了一些“悄悄地”丢失所有权的问题发生,如果需要将所有权进行转移,可以这样操作:

unique_ptr<int> up3 = move(up1); // 现在up3是数据唯一的unique_ptr智能指针
// 或者
unique_ptr<int> up4(move(up1));

只有在使用者显式的调用std::move之后,才会发生所有权的转移,这样就让使用者知道自己在干什么。再来一段代码,看看将unique_ptr作为函数参数和返回值的使用情况:

/*************************************************************************
> File Name: unique_ptrDemo.cpp
> Author: Jelly
> Mail: vipygd#126.com(#->@)
> Created Time: 2014年10月16日 星期四 17时10分49秒
************************************************************************/

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

unique_ptr<int> Func(unique_ptr<int> a)
{         
    cout<<*a<<endl;
    return a;
}         

int main()
{         
    unique_ptr<int> up1(new int(10));

    up1 = Func(move(up1));
    cout<<*up1<<endl;

    return 0;
}

由于在unique_ptr中是没有拷贝构造函数的,所以在直接传参时,进行值传递时,建立临时变量时,就会出错了,所以需要显式的调用move,转移所有权;而函数的返回值已经进行了move操作,而不用显式的进行调用。

8. shared_ptr

在最开始的那段代码中,也简单的使用了一下shared_ptrshared_ptr名如其名,它允许多个该智能指针共享地“拥有”同一堆分配对象的内存;由于它的资源是可以共用的,所以也就可以透过operator=等方法,来分享shared_ptr所使用的资源。由于shared_ptr内部实现上使用的是引用计数这种方法,所以一旦一个shared_ptr指针放弃了“所有权”,其它的shared_ptr对对象的引用并不会发生变化;只有在引用计数归零的时候,shared_ptr才会真正的释放所占有的堆内存空间的。对于引用计数的问题,我这里就不再多总结了,可以参考以下文章:

– 智能指针-引用计数实现
– COM中的引用计数1
– COM中的引用计数2

我这里注重的总结shared_ptr的使用,并不会对shared_ptr进行源码级别的分析。再来一段简单的代码,看看shared_ptr的一些应用。

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

void Func1(shared_ptr<int> a)
{
    cout<<"Enter Func1"<<endl;
    cout<<"Ref count: "<<a.use_count()<<endl;
    cout<<"Leave Func1"<<endl;
}

shared_ptr<int> Func2(shared_ptr<int> a)
{
    cout<<"Enter Func2"<<endl;
    cout<<"Ref count: "<<a.use_count()<<endl;
    cout<<"Leave Func2"<<endl;
    return a;
}

int main()
{
    shared_ptr<int> aObj1(new int(10));
    cout<<"Ref count: "<<aObj1.use_count()<<endl;

    {
        shared_ptr<int> aObj2 = aObj1;
        cout<<"Ref count: "<<aObj2.use_count()<<endl;
    }

    Func1(aObj1);

    Func2(aObj1);

    shared_ptr<int> aObj3 = Func2(aObj1);
    cout<<"Ref count:"<<aObj3.use_count()<<endl;

    return 0;
}

自己单独想想程序的输出。输出如下:

Ref count: 1
Ref count: 2
Enter Func1
Ref count: 2
Leave Func1
Enter Func2
Ref count: 2
Leave Func2
Enter Func2
Ref count: 2
Leave Func2
Ref count:2

9. shared_ptr指向数组

在默认情况下,shared_ptr将调用delete进行内存的释放;当分配内存时使用new[]时,我们需要对应的调用delete[]来释放内存;为了能正确的使用shared_ptr指向一个数组,我们就需要定制一个删除函数,例如:

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

class A
{
public:
    A() { cout<<"constructor"<<endl; }
    ~A() { cout<<"destructor"<<endl; }
};

int main()
{
    shared_ptr<A> arrayObj(new A[5], [](A *p){delete[] p;});

    return 0;
}

上面的代码看不懂的,请参考这篇C++中的Lambda表达式文章。如果确实需要共享地托管一个对象,使用unique_ptr也许会更简单一些,比如:

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

class A
{
public:
    A() { cout<<"constructor"<<endl; }
    ~A() { cout<<"destructor"<<endl; }
};

int main()
{
    unique_ptr<A[]> arrayObj(new A[5]);

    return 0;
}

10. 线程安全

关于多线程中使用shared_ptr,有如下几点描述:

1. 同一个shared_ptr被多个线程读,是线程安全的;
2. 同一个shared_ptr被多个线程写,不是 线程安全的;
3. 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的。 对于第一点,没有什么说的;对于第二点,同一个shared_ptr在不同的线程中进行写操作不是线程安全的,那基于第三点,我们一般会有以下方案来实现线程安全:

对于线程中传入的外部shared_ptr对象,在线程内部进行一次新的构造,例如: sharedptr AObjTmp = outerSharedptrObj;

11. 环形引用

对于使用引用计数实现的智能指针,总是避免不了这个问题的。如果出现类似下面的代码,那就出现了环形引用的问题了。

class Parent
{
public:
    shared_ptr<Child> child;
};

class Child
{
public:
    shared_ptr<Parent> parent;
};

shared_ptr<Parent> pA(new Parent);
shared_ptr<Child> pB(new Child);
pA->child = pB;
pB->parent = pA;

要解决环形引用的问题,没有特别好的办法,一般都是在可能出现环形引用的地方使用weak_ptr来代替shared_ptr。说到了weak_ptr,那下面就接着总结weak_ptr吧。

12. weak_ptr

weak_ptr是最麻烦的,也比较拗口的;它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。但是,使用weak_ptr成员lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回指针空值(nullptr)。由于weak_ptr是指向shared_ptr所指向的内存的,所以,weak_ptr并不能独立存在。例如以下代码:

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

void Check(weak_ptr<int> &wp)
{
    shared_ptr<int> sp = wp.lock(); // 重新获得shared_ptr对象
    if (sp != nullptr)
    {
        cout<<"The value is "<<*sp<<endl;
    }
    else
    {
        cout<<"Pointer is invalid."<<endl;
    }
}

int main()
{
    shared_ptr<int> sp1(new int(10));
    shared_ptr<int> sp2 = sp1;
    weak_ptr<int> wp = sp1; // 指向sp1所指向的内存

    cout<<*sp1<<endl;
    cout<<*sp2<<endl;
    Check(wp);

    sp1.reset();
    cout<<*sp2<<endl;
    Check(wp);

    sp2.reset();
    Check(wp);

    return 0;
}

所以,我们在使用weak_ptr时也要当心,时刻需要判断对应的shared_ptr是否还有效。对于上面的环形引用的问题,由于weak_ptr并不会增加shared_ptr的引用计数,所以我们就可以使用weak_ptr来解决这个问题。

class Parent
{
public:
    weak_ptr<Child> child;
};

class Child
{
public:
    weak_ptr<Parent> parent;
};

shared_ptr<Parent> pA(new Parent);
shared_ptr<Child> pB(new Child);
pA->child = pB;
pB->parent = pA;

 

posted @ 2017-07-24 20:40  糖炒栗子Sugar  阅读(217)  评论(0编辑  收藏  举报