C++智能指针

1.智能指针的由来

C++中,动态内存的管理是通过一对运算符来完成的,new用于申请内存空间,调用对象构造函数初始化对象并返回指向该对象的指针。delete接收一个动态对象的指针,调用对象的析构函数销毁对象,释放与之关联的内存空间。动态内存的管理在实际操作中并非易事,因为确保在正确的时间释放内存是极其困难的,有时往往会忘记释放内存而产生内存泄露;有时在上游指针引用内存的情况下释放了内存,就会产生非法的野指针(悬挂指针)。

为了更容易且更安全的管理动态内存,C++推出了智能指针(smart pointer)类型来管理动态对象。智能指针存储指向动态对象的指针,用于动态对象生存周期的控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露[1]^{[1]}。

对动态内存的管理,可以引申为对系统资源的管理,但是C++程序中动态内存只是最常使用的一种资源,其他常见的资源还包括文件描述符(file descriptor)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets等,这些资源事实上都可以使用智能指针来管理。

2.智能指针的基本思想

智能指针的基本思想是以栈对象管理资源[2]^{[2]}。考察如下示例:

void remodel(std::string & str){
    std::string* ps = new std::string(str);
    ...
    if (weird_thing())
        throw exception();
    str = *ps; 
    delete ps;
    return;
}

如果在函数remodel中出现异常,语句delete ps没有被执行,那么将会导致ps指向的string的堆对象残留在内存中,导致内存泄露。如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,往往会发现内存泄露时有发生,对于程序而言,这无疑是一场灾难!这时我们会想:当remodel这样的函数终止(不管是正常终止,还是由于出现了异常而终止),函数体内的局部变量都将自动从栈内存中删除,因此指针ps占据的内存将被释放,如果ps指向的内存也被自动释放,那该有多好啊。我们知道析构函数有这个功能。如果ps有一个析构函数,该析构函数将在ps过期时自动释放它指向的内存。但ps的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果ps是一个局部的类对象,它指向堆对象,则可以在ps生命周期结束时,让它的析构函数释放它指向的堆对象[3]^{[3]}。

通俗来讲, 智能指针就是模拟指针动作的类。所有的智能指针都会重载->和*操作符。智能指针的主要作用就是用栈智能指针离开作用域自动销毁时调用析构函数来释放资源。当然,智能指针还不止这些,还包括复制时可以修改源对象等。智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数、控制权转移等)。

3.智能指针的引用计数

什么是引用计数? 智能指针有时需要将其管理的对象的所有权转移给其它的智能指针,使得多个智能指针管理同一个对象,比如C++ STL中的shared_ptr支持多个智能指针管理同一个对象。这个时候智能指针就需要知道其引用的对象总共有多少个智能指针在引用在它,也就是说智能指针所管理的对象总共有多少个所有者,我们称之为引用计数(Reference Counting),因为智能指针在准备释放所引用的对象时,如果有其他的智能指针同时在引用这个对象时,则不能释放,而只能将引用计数减一。

引用计数的目的? 引用计数,是资源管理的一种技巧和手段,智能指针使用了引用计数,STL中的string也同样使用了引用计数并配合“写时复制”来实现存储空间的优化。总的来说,使用引用计数有如下两个目的: (1)节省内存,提高程序运行效率。如何很多对象拥有相同的数据实体,存储多个数据实体会造成内存空间浪费,所以最好做法是让多个对象共享同一个数据实体。 (2)记录引用对象的所有者数量,在引用计数为0时,让对象的最后一个拥有者释放对象。

其实,智能指针的引用计数类似于java的垃圾回收机制:java的垃圾的判定很简单,如果一个对象没有引用所指,那么该对象为垃圾。系统就可以回收了。

智能指针实现引用计数的策略。 大多数C++类用三种方法之一来管理指针成员: (1)不管指针成员。复制时只复制指针,不复制指针指向的对象实体。当其中一个指针把其指向的对象的空间释放后,其它指针都成了悬挂指针。这是一种极端做法。 (2)当复制的时候,即复制指针,也复制指针指向的对象。这样可能造成空间的浪费。因为指针指向的对象的复制不一定是必要的。 (3) 第三种就是一种折中的方式。利用一个辅助类来管理指针的复制。原来的类中有一个指针指向辅助类对象,辅助类的数据成员是一个计数器和一个指针(指向原来的对象)。

可见,第三种方法是优先选择的方法,智能指针实现引用计数的策略主要有两种:辅助类与句柄类。使用句柄类尚未研究,本文以辅助类为例,来研究实现智能指针的引用计数。利用辅助类来封装引用计数和指向对象的指针。如此做,指针指针,辅助类对象与被引用对象的关系如下图所示:

辅助类将引用计数与智能指针类指向的对象封装在一起,引用计数记录有多少个智能指针指向同一对象。每次创建智能指针时,初始化智能指针并将引用计数置为1;当智能指针q赋值给另一个智能指针r时,即r=q,拷贝构造函数拷贝智能指针并增加q指向的对象的引用计数,递减r原来指向的对象的引用计数。也就是说对一个智能指针进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数。

4.智能指针的实现模板

智能指针管理对象,本质上是以栈对象来管理堆对象,在《Effective C++》的条款13中称之为资源获取就是初始化(RAII,Resource Acquisition Is Initialization),也就是说我们在获得一笔资源后,尽量以独立的一条语句将资源拿来初始化某个资源管理对象。有时候获得的资源被拿来赋值(而非初始化)某个对管理对象,但不论哪一种做法,获得一笔资源后应该立即放进资源管理对象中。

智能指针就是一种资源管理对象,提供的功能主要有如下几种: (1)以指针的行为方式访问所管理的对象,需要重载指针->操作符; (2)解引用(Dereferencing),获取所管理的对象,需要重载解引用*操作符; (3)智能指针在其声明周期结束时自动销毁其管理的对象; (4)引用计数、写时复制、赋值即释放对象拥有权限、控制权限转移。

第4条是可选功能,拥有第四条中不同的功能对应着不同类型的智能指针,比如C++11在STL中引入的shared_ptr就实现了引用计数的功能,已经被C++11摒弃的auto_ptr[4]^{[4]}实现了赋值即释放对象拥有权限,C++11引入的unique_ptr则实现了控制权限的转移功能。

根据智能指针的功能,通常用类模板实现如下:

template <class T> class SmartPointer {  
private:  
    T *_ptr;  
public:  
    SmartPointer(T *p) : _ptr(p)  //构造函数  
    {  
    }  
    T& operator *()        //重载*操作符  
    {  
        return *_ptr;  
    }  
    T* operator ->()       //重载->操作符  
    {  
        return _ptr;  
    }  
    ~SmartPointer()        //析构函数  
    {  
        delete _ptr;  
    }  
};  

5.通过辅助类模拟实现shared_ptr

STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr,auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提出了unique\ptr作为auto_ptr替代方案。虽然auto_ptr已被摒弃,但在实际项目中仍可使用,但建议使用较新的unique_ptr,因为unique_ptr比auto_ptr更加安全,后文会详细叙述。shared_ptr和weak_ptr则是C+11从准标准库Boost中引入的两种智能指针。此外,Boost库还提出了boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,虽然尚未得到C++标准采纳,但是实际开发工作中可以。

shared_ptr利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个shared_ptr共同管理同一个对象。像shared_ptr这种智能指针,《Effective C++》称之为“引用计数型智能指针”(reference-counting smart pointer,RCSP)。

基础对象类。 首先,我们来定义一个基础对象类Point类,为了方便后面我们验证智能指针是否有效,我们为Point类创建如下接口:

class Point{
private:
    int x, y;
public:
    Point(int xVal = 0, int yVal = 0) :x(xVal), y(yVal) { }
    int getX() const { return x; }
    int getY() const { return y; }
    void setX(int xVal) { x = xVal; }
    void setY(int yVal) { y = yVal; }
};

辅助类。 在创建智能指针类之前,我们先创建一个辅助类。这个类的所有成员皆为私有类型,因为它不被普通用户所使用。为了只为智能指针使用,还需要把智能指针类声明为辅助类的友元。这个辅助类含有两个数据成员:计数count与基础对象指针。也即辅助类用以封装使用计数与基础对象指针。

class RefPtr{
private:
    friend class SmartPtr;      
    RefPtr(Point *ptr):p(ptr),count(1){ }
    ~RefPtr(){delete p;}

    int count;   
    Point *p;                                                      
};

为基础对象类实现智能指针类。 引用计数是实现智能指针的一种通用方法。智能指针将一个计数器与类指向的对象相关联,引用计数跟踪共有多少个类对象共享同一指针。它的具体做法如下: (1)当创建智能指针类的新对象时,初始化指针,并将引用计数设置为1; (2)当能智能指针类对象作为另一个对象的副本时,拷贝构造函数复制副本的指向辅助类对象的指针,并增加辅助类对象对基础类对象的引用计数(加1); (3)使用赋值操作符对一个智能指针类对象进行赋值时,处理复杂一点:先使左操作数的引用计数减1(为何减1:因为指针已经指向别的地方),如果减1后引用计数为0,则释放指针所指对象内存。然后增加右操作数所指对象的引用计数(为何增加:因为此时做操作数指向对象即右操作数指向对象)。 (4)完成析构函数:调用析构函数时,析构函数先使引用计数减1,如果减至0则delete对象。

做好前面的准备后,我们可以来为基础对象类Point书写一个智能指针类了。根据引用计数实现关键点,我们可以写出我们的智能指针类如下:

class SmartPtr{
public:
    SmartPtr(Point *ptr) :rp(new RefPtr(ptr)){} 
    SmartPtr(const SmartPtr &sp):rp(sp.rp){++rp->count;}

    //重载赋值运算符
    SmartPtr& operator=(const SmartPtr& rhs){
        ++rhs.rp->count;    
        if (--rp->count == 0)    
            delete rp;
        rp = rhs.rp;
        return *this;
    }
    //重载->操作符
    Point* operator->(){
        return rp->p;
    }
    //重载*操作符
    Point& operator*(){
        return *(rp->p);
    }

    ~SmartPtr(){       
        if (--rp->count == 0)   
            delete rp;
        else 
            cout << "还有" << rp->count << "个指针指向基础对象" << endl;
    }

private:
    RefPtr *rp;  
};

智能指针类的使用与测试。 至此,我们的智能指针类就完成了,我们可以来看看如何使用。

int main(){
    //定义一个基础对象类指针
    Point *pa = new Point(10, 20);

    //定义三个智能指针类对象,对象都指向基础类对象pa
    //使用花括号控制三个智能指针的生命周期,观察计数的变化
    {
        SmartPtr sptr1(pa);//此时计数count=1
        cout <<"sptr1:"<<sptr1->getX()<<","<<sptr1->getY()<<endl;
        {
            SmartPtr sptr2(sptr1); //调用拷贝构造函数,此时计数为count=2
            cout<<"sptr2:" <<sptr2->getX()<<","<<sptr2->getY()<<endl;
            {
                SmartPtr sptr3=sptr1; //调用赋值操作符,此时计数为conut=3
                cout<<"sptr3:"<<(*sptr3).getX()<<","<<(*sptr3).getY()<<endl;
            }
            //此时count=2
        }
        //此时count=1;
    }
    //此时count=0;pa对象被delete掉
    cout << pa->getX ()<< endl;
    system("pause");
    return 0;
}

来看看运行结果咯:

sptr1:10,20
sptr2:10,20
sptr3:10,20
还有2个指针指向基础对象
还有1个指针指向基础对象
7244864
Press any key to continue . . .

如期,在离开大括号后,共享基础对象的指针从3->2->1->0变换,最后计数为0时,pa对象被delete,此时使用getX()已经获取不到原来的值。

对智能指针的改进。 目前这个智能指针智能用于管理Point类的基础对象,如果此时定义了个矩阵的基础对象类,那不是还得重新写一个属于矩阵类的智能指针类吗?但是矩阵类的智能指针类设计思想和Point类一样啊,就不能借用吗?答案当然是能,那就是使用模板技术。为了使我们的智能指针适用于更多的基础对象类,我们有必要把智能指针类通过模板来实现。这里贴上上面的智能指针类的模板版:

//模板类作为友元时要先有声明
template <typename T> class SmartPtr;

template <typename T> class RefPtr{     //辅助类
private:
    //该类成员访问权限全部为private,因为不想让用户直接使用该类
    friend class SmartPtr<T>;      //定义智能指针类为友元,因为智能指针类需要直接操纵辅助类

    //构造函数的参数为基础对象的指针
    RefPtr(T *ptr) :p(ptr), count(1) { }

    //析构函数
    ~RefPtr() { delete p; }
    //引用计数
    int count;   

    //基础对象指针
    T *p;                                                      
};

template <typename T> class SmartPtr{   //智能指针类
public:
    SmartPtr(T *ptr) :rp(new RefPtr<T>(ptr)) { }      //构造函数
    SmartPtr(const SmartPtr<T> &sp) :rp(sp.rp) { ++rp->count; }  //复制构造函数
    SmartPtr& operator=(const SmartPtr<T>& rhs) {    //重载赋值操作符
        ++rhs.rp->count;        //首先将右操作数引用计数加1,
        if (--rp->count == 0)   //然后将引用计数减1,可以应对自赋值
            delete rp;
        rp = rhs.rp;
        return *this;
    }

    T & operator *()        //重载*操作符  
    {
        return *(rp->p);
    }
    T* operator ->()       //重载->操作符  
    {
        return rp->p;
    }
    ~SmartPtr() {        //析构函数
        if (--rp->count == 0)    //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象
            delete rp;
        else 
        cout << "还有" << rp->count << "个指针指向基础对象" << endl;
    }
private:
    RefPtr<T> *rp;  //辅助类对象指针
};

现在使用智能指针类模板来共享其它类型的基础对象,以int为例:

int main(){
    //定义一个基础对象类指针
    int* ia = new int(10);
    {
        SmartPtr<int> sptr1(ia);
        cout <<"sptr1:"<<*sptr1<<endl;
        {
            SmartPtr<int> sptr2(sptr1); 
            cout <<"sptr2:"<<*sptr2<<endl;
            *sptr2=5;
            {
                SmartPtr<int> sptr3=sptr1; 
                cout <<"sptr3:"<<*sptr3<<endl;
            }
        }
    }
    //此时count=0;pa对象被delete掉
    cout<<*ia<<endl;
    system("pause");
    return 0;
}

测试结果如下:

sptr1:10
sptr2:10
sptr3:5
还有2个指针指向基础对象
还有1个指针指向基础对象
3968064
Press any key to continue . . .

6.STL中另外两种智能指针:unique_ptr与weak_ptr

这里研究一下C++11引入的另外两种智能指针:unique_ptr与weak_ptr。C++准标准库Boost中的智能指针,比如boost::scoped_ptr、boost::shared_array、boost:: intrusive_ptr在这里不做研究,有兴趣的读者可以参考:C++ 智能指针详解

6.1unique_ptr

unique_ptr是一种定义在< memory>中的智能指针(smart pointer)。它持有对对象的独有权——两个unique_ptr不能指向一个对象,即unique_ptr不共享它的所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL) 算法。只能移动 unique_ptr,即对资源管理权限可以实现转。这意味着,内存资源所有权可以将转移到另一个unique_ptr,并且原始 unique_ptr 不再拥有此资源。实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用 make_unique Helper 函数。

下图演示了两个 unique_ptr 实例之间的所有权转换。

unique_ptr与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器很有效,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权,unique_ptr还可能没有对象,这种情况被称为empty。[6]^{[6]}。

unique_ptr的基本操作有:

//智能指针的创建  
unique_ptr<int> u_i; //创建空智能指针
u_i.reset(new int(3)); //"绑定”动态对象  
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d);   //创建空unique_ptr,执行类型为T的对象,用类型为D的对象d来替代默认的删除器delete

//所有权的变化  
int *p_i = u_i2.release(); //释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

6.2为什么要摒弃auto_ptr[3]^{[3]}

unique_ptr 虽然拥有auto_ptr的全部功能,但是为什么摒弃auto_ptr。

(1)基于安全考虑 先来看下面的赋值语句:

auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);
auto_ptr<string> vocation; 
vocaticn = ps;

上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种: (1)定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。 (2)建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。 (3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。

当然,同样的策略也适用于复制构造函数,即auto_ptr<string> vocation(ps)时也需要上面的策略。每种方法都有其用途,但为何要摒弃auto_ptr呢?

下面举个例子来说明。

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

int main() {
    auto_ptr<string> films[5] ={
      auto_ptr<string> (new string("Fowl Balls")),
      auto_ptr<string> (new string("Duck Walks")),
      auto_ptr<string> (new string("Chicken Runs")),
      auto_ptr<string> (new string("Turkey Errors")),
      auto_ptr<string> (new string("Goose Eggs"))
    };
    auto_ptr<string> pwin;
    pwin = films[2]; // films[2] loses ownership. 将所有权从films[2]转让给pwin,此时films[2]不再引用该字符串从而变成空指针

 cout << "The nominees for best avian baseballl film are\n";
 for(int i = 0; i < 5; ++i)
    cout << *films[i] << endl;
 cout << "The winner is " << *pwin << endl;
 cin.get();
 return 0;
}

运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:

使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。

使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译期因下述代码行出现错误:

unique_ptr<string> pwin;
pwin = films[2]; // films[2] loses ownership

指导你发现潜在的内存错误。这就是为何要摒弃auto_ptr的原因,一句话总结就是:避免因潜在的内存问题导致程序崩溃

从上面可见,unique_ptr比auto_ptr更加安全,因为auto_ptr有拷贝语义,拷贝后原象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr则禁止了拷贝语义,但提供了移动语义,即可以使用std::move()进行控制权限的转移,如下代码所示:

unique_ptr<string> upt(new string("lvlv"));
unique_ptr<string> upt1(upt);   //编译出错,已禁止拷贝
unique_ptr<string> upt1=upt;    //编译出错,已禁止拷贝
unique_ptr<string> upt1=std::move(upt);  //控制权限转移

auto_ptr<string> opt(new string("lvlv"));
auto_ptr<string> opt1(opt); //编译通过
auto_ptr<string> opt1=opt;  //编译通过

这里要注意,在使用std::move将unique_ptr的控制权限转移后,不能够在通过unique_ptr来访问和控制资源了,否则同样会出现程序崩溃。我查了一下在使用unique_ptr来访问资源前,是否有判断的API,可惜查阅了C++ reference,并没有发现unique_ptr提供判空接口,希望C++标准以后能够继续完善unique_ptr。

(2)unique_ptr不仅安全,而且灵活 如果unique_ptr 是个临时右值,编译器允许拷贝语义。参考如下代码:

unique_ptr<string> demo(const char * s){
    unique_ptr<string> temp (new string (s)); 
    return temp;
}

//假设编写了如下代码:
unique_ptr<string> ps;
ps = demo('Uniquely special");

demo()返回一个临时unique_ptr,然后ps接管了原本归返回的unique_ptr所有的对象,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值。相对于auto_ptr任何情况下都允许拷贝语义,这正是unique_ptr更加灵活聪明的地方。

(3)扩展auto_ptr不能完成的功能 (3.1)unique_ptr可放在容器中,弥补了auto_ptr不能作为容器元素的缺点。

//方式一:
vector<unique_ptr<string>> vs { new string{“Doug”}, new string{“Adams”} };  

//方式二:
vector<unique_ptr<string>>v;  
unique_ptr<string> p1(new string("abc"));  

(3.2)管理动态数组,因为unique_ptr有unique_ptr

unique_ptr<int[]> p (new int[3]{1,2,3});  
p[0] = 0;// 重载了operator[]  

(3.3)自定义资源删除操作(Deleter)。unique_ptr默认的资源删除操作是delete/delete[],若需要,可以进行自定义:

void end_connection(connection *p) { disconnect(*p); } //资源清理函数  

//资源清理器的“类型” 
unique_ptr<connection, decltype(end_connection)*> p(&c, end_connection);// 传入函数名,会自动转换为函数指针  

综上所述,基于unique_ptr的安全性和扩充的功能,unique_ptr成功的将auto_ptr取而代之。

6.3weak_ptr

6.3.1weak_ptr简介

weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,因此取名为weak,表明其是功能较弱的智能指针。它的最大作用在于协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 boost::weak_ptr 只对 boost::shared_ptr 进行引用,而不改变其引用计数,当被观察的 boost::shared_ptr 失效后,相应的 boost::weak_ptr 也相应失效。

6.3.2用法

使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr管理的对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。总结来说,weak_ptr的基本用法总结如下:

weak_ptr<T> w;      //创建空weak_ptr,可以指向类型为T的对象。
weak_ptr<T> w(sp);  //与shared_ptr指向相同的对象,shared_ptr引用计数不变。T必须能转换为sp指向的类型。
w=p;                //p可以是shared_ptr或weak_ptr,赋值后w与p共享对象。
w.reset();          //将w置空。
w.use_count();      //返回与w共享对象的shared_ptr的数量。
w.expired();        //若w.use_count()为0,返回true,否则返回false。
w.lock();           //如果expired()为true,返回一个空shared_ptr,否则返回非空shared_ptr。

下面是一个简单的使用示例:

#include < assert.h>

#include <iostream>
#include <memory>
#include <string>
using namespace std;
int main(){
    shared_ptr<int> sp(new int(10));
    assert(sp.use_count() == 1);
    weak_ptr<int> wp(sp); //从shared_ptr创建weak_ptr
    assert(wp.use_count() == 1);
    if (!wp.expired())//判断weak_ptr观察的对象是否失效
    {
        shared_ptr<int> sp2 = wp.lock();//获得一个shared_ptr
        *sp2 = 100;
        assert(wp.use_count() == 2);
    }
    assert(wp.use_count()== 1);
    cout<<"int:"<<*sp<<endl;
    system("pause");
    return 0;
}

程序输出: int:100

从上面可以看到,尽管以shared_ptr来构造weak_ptr,但是weak_ptr内部的引用计数并没有什么变化。

6.3.3weak_ptr的作用

现在要说的问题是,weak_ptr到底有什么作用呢?从上面那个例子看来,似乎没有任何作用。其实weak_ptr可用于打破循环引用。引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引用的对象。一个简单的例子如下:

#include <iostream>  
#include <memory>  

class Woman;  
class Man{  
private:  
    //std::weak_ptr<Woman> _wife;  
    std::shared_ptr<Woman> _wife;  
public:  
    void setWife(std::shared_ptr<Woman> woman){  
        _wife = woman;  
    }  

    void doSomthing(){  
        if(_wife.lock()){  
        }  
    }  

    ~Man(){  
        std::cout << "kill man\n";  
    }  
};  

class Woman{  
private:  
    //std::weak_ptr<Man> _husband;  
    std::shared_ptr<Man> _husband;  
public:  
    void setHusband(std::shared_ptr<Man> man){  
        _husband = man;  
    }  
    ~Woman(){  
        std::cout <<"kill woman\n";  
    }  
};  


int main(int argc, char** argv){  
    std::shared_ptr<Man> m(new Man());  
    std::shared_ptr<Woman> w(new Woman());  
    if(m && w) {  
        m->setWife(w);  
        w->setHusband(m);  
    }  
    return 0;  
}  

在Man类内部会引用一个Woman,Woman类内部也引用一个Man。当一个man和一个woman是夫妻的时候,他们直接就存在了相互引用问题。man内部有个用于管理wife生命期的shared_ptr变量,也就是说wife必定是在husband去世之后才能去世。同样的,woman内部也有一个管理husband生命期的shared_ptr变量,也就是说husband必须在wife去世之后才能去世。这就是循环引用存在的问题:husband的生命期由wife的生命期决定,wife的生命期由husband的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。

一般来讲,解除这种循环引用有下面三种可行的方法: (1)当只剩下最后一个引用的时候需要手动打破循环引用释放对象。 (2)当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。 (3)使用弱引用的智能指针打破这种循环引用。 虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法,使用弱引用的智能指针std:weak_ptr来打破循环引用。

weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。做法就是上面的代码注释的地方取消注释,取消Woman类或者Man类的任意一个即可,也可同时取消注释,全部换成弱引用weak_ptr。

另外很自然地一个问题是:既然weak_ptr不增加资源的引用计数,那么在使用weak_ptr对象的时候,资源被突然释放了怎么办呢?呵呵,答案是你根本不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是:在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()方法。

注意:shared_ptr实现了operator bool() const方法来判断一个管理的资源是否被释放。

7.如何选择智能指针?

在掌握了上面提到的C++ STL中的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?

下面给出几个使用指南。 (1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括: (1.1)有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素; (1.2)两个对象都包含指向第三个对象的指针; (1.3)STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。 (2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋值给另一个的算法(如sort())。例如,可在程序中使用类似于下面的代码段。

unique_ptr<int> make_int(int n)
{
    return unique_ptr<int>(new int(n));
}
void show(unique_ptr<int> &p1)
{
    cout << *a << ' ';
}
int main()
{
    ...
    vector<unique_ptr<int> > vp(size);
    for(int i = 0; i < vp.size(); i++)
        vp[i] = make_int(rand() % 1000);       //copy temporary unique_ptr
    vp.push_back(make_int(rand() % 1000));     //ok because arg is temporary
    for_each(vp.begin(), vp.end(), show);      //use for_each()
    ...
}

其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。

在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给另一个unique_ptr需要满足的条件相同,即unique_ptr必须是一个临时的对象。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr< int>:

unique_ptr<int> pup(make_int(rand() % 1000));   // ok
shared_ptr<int> spp(pup);                       // not allowed, pup as lvalue
shared_ptr<int> spr(make_int(rand() % 1000));   // ok

模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。

在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。


参考文献

[1]Stanley B.Lippman著,王刚,杨巨峰译.C++ Primer(第五版).2013:400-422 [2]Scott Meyers著,侯捷译.Effective C++中文版(第三版).2011:61-77 [3]C++智能指针简单剖析 [4]shared_ptr基于引用计数智能指针实现 [5] C++中智能指针的设计和使用 [6]C++11智能指针之unique_ptr [7]Boost智能指针——weak_ptr [8]std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

这段代码很Pythonic | 相见恨晚的 itertools 库

17830
来自专栏数据科学与人工智能

【Python环境】Python函数式编程指南(1):概述

1. 函数式编程概述 1.1. 什么是函数式编程? 函数式编程使用一系列的函数解决问题。函数仅接受输入并产生输出,不包含任何能影响产生输出的内部状态。任何情况下...

23260
来自专栏xingoo, 一个梦想做发明家的程序员

《JavaScript语言精粹》—— 读书总结

话说这本书还是同学的推荐才读的,之前感觉这本书太薄了,不值得看,没想到小身材有大智慧,书中的内容总结的还是很到位的!所以就把最后几章,精华的部分整理整理。 优...

28190
来自专栏向治洪

迭代器模式

迭代器模式(Iterator): 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。 用途:在软件构建过程中,集合对象内部结构常常变化各异。...

198100
来自专栏高性能分布式系统设计

从设计上规避Go语言的 interface{} == nil 判断容易出错的简单办法

Go语言的interface{} 本质上是一个结构,含有一个type字段,一个pointer字段。 很多初学者会拿一个已经在某种情况下被赋予类型的interfa...

32250
来自专栏龙首琴剑庐

Java总论及三大特性理解

1、对象(object)     万物皆为对象(根类Object类)。     程序是对象的集合(面向对象程序设计语言OOP)。     每个对象都有自己的由其...

31160
来自专栏WindCoder

日期判断

8510
来自专栏小鄧子的技术博客专栏

为什么android API 中有很多对象的创建都是使用new关键字

首先,谢邀。 其次,是怎么找到我知乎账号的,我隐藏的这么深(脸红了) 最后,加入了自己的总结概括,让然也可以当成读书笔记来看。

11930
来自专栏北京马哥教育

Python新手常见错误之默认值设定错误

文章来源 |伯乐在线 云豆贴心提醒,本文阅读时间5分钟,文末有秘密! Python初学者通常会犯一些错误,甚至会因此损失很大的自信心。 不过你不必过多的担...

35760
来自专栏小樱的经验随笔

【批处理学习笔记】第二十一课:数值计算

    批处理里面的数值计算功能较弱,只能够进行整型计算,忽略浮点数的小数部分;同时数值计算的范围也受限于系统位数,对于目前较为常见的32位机来说,数值计算能处...

29040

扫码关注云+社区

领取腾讯云代金券