智能指针(Smart pointer)是一种抽象的数据类型。在程序设计中,它通常是经由类别模板(class template)来实做,借由模板(template)来达成泛型,通常借由类别(class)的析构函数来达成自动释放指针所指向的内存或对象。

auto_ptr[c++11弃用]unique_ptr[c++11支持] shared_ptr[c++11支持]weak_ptr[c++11支持]

auto_ptr

namespace std {

    template <class Y> struct auto_ptr_ref {};

    template <class X>
    class auto_ptr {
    public:
        typedef X element_type;

        // 20.4.5.1 construct/copy/destroy:
        explicit           auto_ptr(X* p =0)throw();
                           auto_ptr(auto_ptr&)throw();
        template <class Y> auto_ptr(auto_ptr<Y>&)throw();

        auto_ptr&                      operator=(auto_ptr&)throw();
        template <class Y> auto_ptr&   operator=(auto_ptr<Y>&)throw();
        auto_ptr&                      operator=(auto_ptr_ref<X>)throw();

        ~auto_ptr() throw();

        // 20.4.5.2 members:
        X&     operator*() const throw();
        X*     operator->() const throw();
        X*     get() const throw();
        X*     release() throw();
        void   reset(X* p =0)throw();

        // 20.4.5.3 conversions:
        auto_ptr(auto_ptr_ref<X>)throw();
        template <class Y> operator auto_ptr_ref<Y>() throw();
        template <class Y> operator auto_ptr<Y>() throw();
    };

}

 

unique_ptr

独占使用(人无我有,人有我丢)

C++11新增了move语义,相比copy语义,它能更好的实现值传递。

std::auto_ptr使用的是copy语义,为了向前兼容,C++11没有修改std::auto_ptr,而是引入了新的使用move语义的std::unique_ptr

unique_ptr是用于取代c++98的auto_ptr的产物,在c++98的时候还没有移动语义(move semantics)的支持,因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把auto_ptr作为函数参数,传进去的时候控制权转移,转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的auto_ptr已经失效了。在c++11当中有了移动语义,使用move()把unique_ptr传入函数。移动语义本身就说明了这样的问题,比较坑爹的是标准描述是说对于move之后使用原来的内容是未定义行为,并非抛出异常,所以还是要靠人肉遵守游戏规则。再一个,auto_ptr不支持传入deleter,所以只能支持单对象(delete object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等。

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

  1. 拥有它指向的对象
  2. 无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作
  3. 保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

  1. 为动态申请的内存提供异常安全
  2. 将动态申请的内存所有权传递给某函数
  3. 从某个函数返回动态申请内存的所有权
  4. 在容器中保存指针
  5. auto_ptr 应该具有的功能

unique_ptr的拷贝构造函数和赋值运算符都声明为deleted,也就是说它不能被拷贝,只能通过std::move来转递它所指向的内存的所有权。

std::unique_ptr<int> p1(new int(5));
std::unique_ptr<int> p2 = p1; // 编译会出错
std::unique_ptr<int> p3 = std::move (p1); // 转移所有权,现在那块内存归p3所有, p1成为无效的指针。

p3.reset(); //释放内存。
p1.reset(); //实际上什么都没做。

 

shared_ptr和weak_ptr

如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查。

shared_ptr(你有我有全都有),好比公司有好几个股东,其中一个股东死了,公司还能继续存在。所有股东都死了,公司才能关门。

weak_ptr 是给员工用的,员工也能代表公司干点活,可以有很多,但没卵用。公司要关门了,不管还有多少员工,一并开除。

std::shared_ptr使用引用计数。每一个shared_ptr的拷贝都指向相同的内存。在最后一个shared_ptr析构的时候,内存才会被释放。

std::shared_ptr<int> p1(new int(5));
std::shared_ptr<int> p2 = p1; // 都指向同一内存。

p1.reset(); // 因为p2还在,所以内存没有释放。
p2.reset(); // 释放内存,因为没有shared_ptr指向那块内存了。

顾名思义, weak_ptr是一个弱引用,只引用,不计数。std::shared_ptr使用引用计数,有循环计数的问题。为了打破循环,可以使用std::weak_ptr.

std::shared_ptr<int> p1(new int(5));
std::weak_ptr<int> wp1 = p1; // 还是只有p1有所有权。

{
  std::shared_ptr<int> p2 = wp1.lock(); // p1和p2都有所有权
  if (p2) // 使用前需要检查
  { 
    // 使用p2
  }
} // p2析构了,现在只有p1有所有权。

p1.reset(); // 内存被释放。

std::shared_ptr<int> p3 = wp1.lock(); // 因为内存已经被释放了,所以得到的是空指针。
ifp3
{
  // 不会执行到这。
}

 

有时对象必须存储一种方法,用来在不引起引用计数增加的情况下访问 shared_ptr 的基础对象。通常,在 shared_ptr 实例之间循环引用时,就会出现此情况。

  • 尽可能地避免指针具有共享所有权。
  • 如果必须具有共享的 shared_ptr 实例所有权,避免在实例之间进行循环引用。如果循环引用不可避免,甚至由于某种原因而更为可取,请使用 weak_ptr 为一个或多个所有者提供对其他 shared_ptr 的弱引用。

使用 weak_ptr,可以创建连接到现有相关实例组的 shared_ptr,但仅当基础内存资源有效时才行。 weak_ptr 本身并不参与引用计数,因此,它无法阻止引用计数转到为零。但是可以使用 weak_ptr 来尝试获取 shared_ptr 的新副本,通过使用该副本进行初始化。如果内存已被删除,则会引发 bad_weak_ptr 异常。如果内存仍有效,则新的共享指针会递增引用计数,并确保只要 shared_ptr 变量保持在范围内,内存就有效。

下面的代码示例演示了使用 weak_ptr 以确保正确删除循环依赖关系对象的实例。

Example:

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <algorithm>

using namespace std;

class Controller 
{
public:
    int Num;
    wstring Status;
    vector<weak_ptr<Controller>> others;
    explicit Controller(int i) : Num(i) , Status(L"On")
    {
        wcout << L"Creating Controller" << Num << endl;
    }

    ~Controller()
    {
        wcout << L"Destroying Controller" << Num << endl;
    }

    // Demonstrates how to test whether the  
    // pointed-to memory still exists or not. 
    void CheckStatuses() const
    {
        for_each(others.begin(), others.end(), [] (weak_ptr<Controller> wp)
        {
            try
            {
                auto p = wp.lock();
                wcout << L"Status of " << p->Num << " = " << p->Status << endl;
            }

            catch (bad_weak_ptr b)
            {
                wcout << L"Null object" << endl;
            }                
        });
    }
};

void RunTest()
{
    vector<shared_ptr<Controller>> v;

    v.push_back(shared_ptr<Controller>(new Controller(0)));
    v.push_back(shared_ptr<Controller>(new Controller(1)));
    v.push_back(shared_ptr<Controller>(new Controller(2)));
    v.push_back(shared_ptr<Controller>(new Controller(3)));
    v.push_back(shared_ptr<Controller>(new Controller(4)));

    // Each controller depends on all others not being deleted. 
    // Give each controller a pointer to all the others. 
    for (int i = 0 ; i < v.size(); ++i)
    {
        for_each(v.begin(), v.end(), [v,i] (shared_ptr<Controller> p)
        {
            if(p->Num != i)
            {
                v[i]->others.push_back(weak_ptr<Controller>(p));
                wcout << L"push_back to v[" << i << "]: " << p->Num << endl;
            }
        });        
    }

    for_each(v.begin(), v.end(), [](shared_ptr<Controller>& p)
    {
        wcout << L"use_count = " << p.use_count() << endl;
        p->CheckStatuses();
    });
}

int main()
{    
    RunTest(); 
    wcout << L"Press any key" << endl;
    char ch;
    cin.getline(&ch, 1);
}
Result:

Creating Controller0
Creating Controller1
Creating Controller2
Creating Controller3
Creating Controller4
push_back to v[0]: 1
push_back to v[0]: 2
push_back to v[0]: 3
push_back to v[0]: 4
push_back to v[1]: 0
push_back to v[1]: 2
push_back to v[1]: 3
push_back to v[1]: 4
push_back to v[2]: 0
push_back to v[2]: 1
push_back to v[2]: 3
push_back to v[2]: 4
push_back to v[3]: 0
push_back to v[3]: 1
push_back to v[3]: 2
push_back to v[3]: 4
push_back to v[4]: 0
push_back to v[4]: 1
push_back to v[4]: 2
push_back to v[4]: 3
use_count = 1
Status of 1 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 2 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 3 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 4 = On
use_count = 1
Status of 0 = On
Status of 1 = On
Status of 2 = On
Status of 3 = On
Destroying Controller0
Destroying Controller1
Destroying Controller2
Destroying Controller3
Destroying Controller4
Press any key

 

C++中存在两种语义:值语义(value sematics)和对象语义(object sematic),对象语义也可以叫做引用语义(reference sematics)。

  • 值语义,指的是对象的拷贝与原对象无关,就像拷贝int一样,C++的常用类型数据等都是值语义。
  • 对象语义,指的是面向对象意义下的对象,是禁止拷贝的

在设计一个类的时候该类是否可以被拷贝(即具备拷贝构造函数),取决于拷贝后的语义是否成立。比如一个Thread类,拷贝后系统中并不会启动另外一个线程,所以拷贝是禁止的。这么设计起码有两个好处:

  • 语义合理,有些对象复制是不符合常理的
  • 节省内存

 

强引用

当对象被创建时,计数为1;每创建一个变量引用该对象时,该对象的计数就增加1;当上述变量销毁时,对象的计数减1,当计数为0时,这个对象也就被析构了。
强引用计数在很多种情况下都是可以正常工作的,但是也有不凑效的时候,当出现循环引用时,就会出现严重的问题,以至于出现内存泄露,如下代码:
#include <string>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/weak_ptr.hpp>

class parent;
class children;

typedef boost::shared_ptr<parent> parent_ptr;
typedef boost::shared_ptr<children> children_ptr;

class parent
{
public:
    ~parent() { std::cout <<"destroying parent\n"; }

public:
    children_ptr children;
};

class children
{
public:
    ~children() { std::cout <<"destroying children\n"; }

public:
    parent_ptr parent;
};

void test()
{
    parent_ptr father(new parent());
    children_ptr son(new children);

    father->children = son;
    son->parent = father;
}

void main()
{
    std::cout<<"begin test...\n";
    test();
    std::cout<<"end test.\n";
}

运行该程序可以看到,即使退出了test函数后,由于parent和children对象互相引用,它们的引用计数都是1,不能自动释放,并且此时这两个对象再无法访问到。这就引起了c++中那臭名昭著的内存泄漏。
一般来讲,解除这种循环引用有下面有三种可行的方法:

  1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
  2. 当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
  3. 使用弱引用的智能指针打破这种循环引用。

虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。

 

弱引用

boost::weak_ptr<T>是boost提供的一个弱引用的智能指针,它的声明可以简化如下:
namespace boost {

    template<typename T> class weak_ptr {
    public:
        template <typename Y>
        weak_ptr(const shared_ptr<Y>& r);

        weak_ptr(const weak_ptr& r);

        ~weak_ptr();

        T* get() const; 
        bool expired() const; 
        shared_ptr<T> lock() const;
    }; 
}

可以看到,boost::weak_ptr必须从一个boost::share_ptr或另一个boost::weak_ptr转换而来,这也说明,进行该对象的内存管理的是那个强引用的boost::share_ptr。boost::weak_ptr只是提供了对管理对象的一个访问手段。boost::weak_ptr除了对所管理对象的基本访问功能(通过get()函数)外,还有两个常用的功能函数:expired()用于检测所管理的对象是否已经释放;lock()用于获取所管理的对象的强引用指针。
由于弱引用不更改引用计数,类似普通指针,只要把循环引用的一方使用弱引用,即可解除循环引用。对于上面的那个例子来说,只要把children的定义改为如下方式,即可解除循环引用:
class children
{
public:
    ~children() { std::cout <<"destroying children\n"; }

public:
    boost::weak_ptr<parent> parent;
};
最后值得一提的是,虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说这个仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏。毕竟,对于C++来说,由于没有垃圾回收机制,内存泄漏对每一个程序员来说都是一个非常头痛的问题。
 
  • 指针变量是否需要拥有资源的所有权?

如果指针变量需要绑定资源的所有权,那么选择unique_ptr、shared_ptr。它们可以通过RAII完成对资源生命期的自动管理。

如果指针变量不需要拥有资源的所有权,那么选择weak_ptr、raw pointer,这两种指针变量在离开作用域时不会对其所指向的资源产生任何影响。

  • 如果不拥有资源的所有权(non-owning pointer),那么指针变量是否需要在适当的时候感知到资源的有效性?

如果需要则使用weak_ptr,通过weak_ptr::lock()获得所有权,当拥有所有权后便可以得知资源的有效性。

  • 为何unique_ptr不能和weak_ptr配合?

unique_ptr是独占所有权,也就是说资源的生命期等于指针变量的生命期,那么程序员可以通过指针变量的生命期判断资源是否有效,这样weak_ptr就不再有必要了。而相对来说,shared_ptr则不好判断,特别是多线程环境下。

posted on 2016-07-18 14:32  逸蒙  阅读(179)  评论(0编辑  收藏  举报