C++智能指针系列:其三、shared_ptr

前面提到的两种智能指针,设计思想都是独占资源。shared_ptr的设计思想是共享资源。一个指针可以被多个shared_ptr管理。采用引用计数法控制析构函数的执行,当shared_ptr发生赋值和拷贝的时候,引用计数加一。当shared_ptr发生析构的时候,引用计数减一。这个看起来特别像jvm里面的引用计数法,实则不然。后文会对原理有详细介绍。

一、shared_ptr的初始化

int main ( )
{
    shared_ptr<Test> sp1(new Test);    //构造函数初始化

    shared_ptr<Test> sp2;          //reset初始化
    sp2.reset(new Test);

    shared_ptr<Test> sp3 = make_shared<Test>();     //make_shared初始化

    shared_ptr<Test> sp4(sp1);      //拷贝构造函数初始化

    shared_ptr<Test> sp5;          //赋值运算符重载初始化
    sp5 = sp1;

    shared_ptr<Test> sp6(move(sp1)); //移动构造函数初始化

    shared_ptr<Test> sp7;          //右值赋值运算符重载初始化
    sp7 = move(sp2);               
}

主要有三种初始化方法:
(1)构造函数初始化;
(2)对于已经构造的shared_ptr对象,可以采用reset的方法重新托管新的内存。
(3)使用make_shared方法初始化,<>中记录托管的指针类型,()中表示托管类型的构造函数参数。后面会指出,优先使用方法(3)
(4)(5)(6)(7)后文有详细解释。

二、引用计数的使用

可以使用use_count函数可以获得当前托管指针的引用计数。

int main ( )
{
    shared_ptr<Test> sp1(new Test);
    cout << "sp1	use_count() = " << sp1.use_count() << endl<<endl;

    shared_ptr<Test> sp2(sp1);
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl<<endl;

    shared_ptr<Test> sp3;
    sp3 = sp1;
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl;
    cout << "sp3	use_count() = " << sp3.use_count() << endl<<endl;

    sp1.reset();
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl;
    cout << "sp3	use_count() = " << sp3.use_count() << endl<<endl;

    sp2= nullptr;
    cout << "sp1	use_count() = " << sp1.use_count() << endl;
    cout << "sp2	use_count() = " << sp2.use_count() << endl;
    cout << "sp3	use_count() = " << sp3.use_count() << endl<<endl;

}

输出结果:

Test start...
sp1     use_count() = 1

sp1     use_count() = 2
sp2     use_count() = 2

sp1     use_count() = 3
sp2     use_count() = 3
sp3     use_count() = 3

sp1     use_count() = 0
sp2     use_count() = 2
sp3     use_count() = 2

sp1     use_count() = 0
sp2     use_count() = 0
sp3     use_count() = 1

Test end...

对于拷贝构造函数与赋值之后,引用计数加一,这个绝对没问题,但是如果是移动构造函数与右值赋值运算符重载呢?

int main ( )
{
    shared_ptr<Test> sp1(new Test);
    cout << "sp1	use_count() = " << sp1.use_count() << endl<<endl;

    shared_ptr<Test> sp2(move(sp1));
    cout << "sp1	use_count() = " << sp1.use_count() <<"  ptr= "<<sp1.get()<< endl;
    cout << "sp2	use_count() = " << sp2.use_count() <<"  ptr= "<<sp2.get()<< endl<<endl;

    shared_ptr<Test> sp3;
    sp3 = move(sp2);
    cout << "sp2	use_count() = " << sp2.use_count() <<"  ptr= "<<sp2.get()<< endl;
    cout << "sp3	use_count() = " << sp3.use_count() <<"  ptr= "<<sp3.get()<< endl<<endl;
}

运行结果:

Test start...
sp1     use_count() = 1

sp1     use_count() = 0  ptr= 0
sp2     use_count() = 1  ptr= 0x1ed7a6a18e0

sp2     use_count() = 0  ptr= 0
sp3     use_count() = 1  ptr= 0x1ed7a6a18e0

Test end...

可以看出,对于移动构造函数与右值赋值运算符重载,引用计数不变,被move的智能指针放弃了对于实际指针的托管。不看源码也能猜到,是在shared_ptr自定义的移动构造函数与右值赋值运算符重载函数中做的事情。
看到这里可能会发现,漏掉了一个关键的函数:release()。实际上,在shared_ptr中没有实现release函数,为什么呢?release函数所做的事情是:转移被托管的指针。转移给谁了不确定,可能是另一个shared_ptr对象,也可能是裸指针。这就不太好控制引用计数了。
引用计数总结如下:
(1)构造shared_ptr对象时,传入一个非空指针,引用计数初始化=1;
(2)根据已有的shared_ptr对象进行拷贝构造、赋值操作的时候,引用计数+1;
(3)当shared_ptr转而管理其他指针时(一般通过reset函数),或者直接置为nullptr时,引用计数-1;
(4)根据已有shared_ptr对象(代号a)的右值进行移动构造函、右值赋值的时候,对象a托管的指针置为空指针,原来的指针的引用计数不变。

三、引用计数的实现原理

在古早版本的java语言中,采用引用计数法进行垃圾回收,基本原理和shared_ptr类似,但是不一样的地方在于,java语言中的计数是放在每个对象里面,有jvm维护一个用户不可感知的计数变量。由于C++语言历史包袱过于沉重,很难从编译器层面做出改变,shared_ptr实际上是编码层面的设计,由shared_ptr对象维护计数变量。
初学者容易产生误解,这个计数变量应该是静态变量?当然不可能是静态变量,因为一个实例化后的shared_ptr可能指向多个指针,不可能统一计数。
它的做法非常简单,shared_ptr对象维护一个被托管指针的同时,维护一个int指针,记录被托管指针被引用的次数。如果有多个shared_ptr托管这个指针的时候,他们都可以修改这个int值(因为是指针),修改之后也可以被其他对象感知到。
(1)通过有参构造函数初始化的时候,给指针变量赋初值,初始化记录引用次数的int指针。
(2)根据shared_ptr A,进行拷贝构造的时候,构造shared_ptr B,B记录的被托管指针赋值为A所记录的,同时,B的计数指针被赋值为A的计数指针,并且指针指向的int值加一。
(3)赋值运算符重载操作同上,但是注意一点,如果判断A和B托管的指针是同一个,函数立刻终止。如果不是同一个,A的托管的原指针对应的计数-1,如果减一后等于0,触发析构函数。
(4)根据shared_ptr A,进行移动构造的时候,构造shared_ptr B,B记录的被托管指针赋值为A所记录的,同时,B的计数指针被赋值为A的计数指针。之后A托管的指针置为null,A记录的引用计数指针也置为null。右值赋值同理。
(5)当shared_ptr A调用reset重置的时候,放弃原来被托管指针,原引用计数-1,如果减一后=0,触发析构函数。置为nullptr同理。
上述方案,其实是虚假的引用计数法,与java的引用计数法完全不能相提并论,有一个非常非常致命的缺陷,后面会提到。

四、自己实现shared_ptr。

template<typename Ty>     //Ty是被托管的指针类型
class my_shared_ptr{
private:
    Ty * ty;     //被托管的指针
    int * count;
    void release(){
        (*count)--;
        if((*count)==0)
        {
            if(ty)
            {
                delete ty;
                ty=NULL;
            }
            if(count)
            {
                delete count;
                count=NULL;
            }
        }
    }
public:
    explicit my_shared_ptr(Ty * ty1=0):ty(ty1){
        if(ty!= nullptr){
            count = new int(1);
        }
    }

    ~my_shared_ptr(){  //析构函数中释放资源
        (*count)--;
        if((*count)==0)
        {
            if(ty)
            {
                delete ty;
                ty=NULL;
            }
            if(count)
            {
                delete count;
                count=NULL;
            }
        }
    }

    Ty * get(){          //返回被托管的指针
        return ty;
    }

    void reset(Ty * ty2){      //重置被托管的指针
        if(ty!=ty2){
            release();
            ty=ty2;
            if(ty!= nullptr){
                count = new int(1);
            }
        }
    }

    Ty * operator ->(){       //重载->和*运算符,让智能指针用起来像指针
        return ty;
    }

    Ty & operator * (){
        return *(ty);
    }

    my_shared_ptr(my_shared_ptr && ty1){     //移动构造函数
        release();
        ty = ty1.ty;
        count = ty1.count;
        ty1.ty= nullptr;
        ty1.count = nullptr;
    }

    my_shared_ptr& operator = (my_shared_ptr && ty1 ){   //移动赋值运算符重载
        release();
        ty = ty1.ty;
        count = ty1.count;
        ty1.ty= nullptr;
        ty1.count = nullptr;
        return *this;
    }

    my_shared_ptr& operator = (my_shared_ptr & ty1 ) {  //左值赋值运算符重载
        release();
        ty = ty1.ty;
        count = ty1.count;
        (*count)++;
        return *this;
    }

    my_shared_ptr(my_shared_ptr & ty1) {   //左值拷贝构造函数
        ty = ty1.ty;
        count = ty1.count;
        (*count)++;
    }
};

五、shared_ptr作为形参

前文提到了,auto_ptr作为形参是一件非常危险的事情,因为其资源转移的特点,会导致访问非法内存。shared_ptr不用考虑这个,看代码:

void my_ptr_test(shared_ptr<Test> ptr) {
    cout<<"when call my_ptr_test, use_count = "<<ptr.use_count()<<endl;
}

int main ( )
{
    shared_ptr<Test> test(new Test);
    cout<<"before call my_ptr_test, use_count = "<<test.use_count()<<endl;
    my_ptr_test(test);
    cout<<"after call my_ptr_test, use_count = "<<test.use_count()<<endl;
}

运行结果:

Test start...
before call my_ptr_test, use_count = 1
when call my_ptr_test, use_count = 2
after call my_ptr_test, use_count = 1
Test end...

显然,赋值给形参以后,仍然出现了引用计数+1,但是函数执行完毕,形参销毁,引用计数-1.所以这个过程程序员完全可以不care,编译器已经帮我们处理好了。

六、shared_ptr的缺陷

(1)循环引用导致无法释放内存

class Girl;

class Boy {
public:
    Boy() {
        cout << "Boy Start" << endl;
    }

    ~Boy() {
        cout << "~Boy End" << endl;
    }

    void setGirlFriend(shared_ptr<Girl> _girlFriend) {
        this->girlFriend = _girlFriend;
    }

private:
    shared_ptr<Girl> girlFriend;
};

class Girl {
public:
    Girl() {
        cout << "Girl Start" << endl;
    }

    ~Girl() {
        cout << "~Girl End" << endl;
    }

    void setBoyFriend(shared_ptr<Boy> _boyFriend) {
        this->boyFriend = _boyFriend;
    }

private:
    shared_ptr<Boy> boyFriend;
};


void useTrap() {
    shared_ptr<Boy> spBoy(new Boy());
    shared_ptr<Girl> spGirl(new Girl());

    // 陷阱用法
    spBoy->setGirlFriend(spGirl);
    spGirl->setBoyFriend(spBoy);
    // 此时boy和girl的引用计数都是2
}


int main(void) {
    useTrap();
    return 0;
}

可以使用weak_ptr解决这个问题。下一篇博客有详细介绍。

(2)多次释放内存
循环引用是引用计数法的自身缺陷,所以在现在版本的jvm中已经抛弃了这种垃圾回收机制。但是多次释放内存完全是shared_ptr的设计缺陷,因此,shared_ptr使用的引用计数法是虚假的引用计数法。

int main(void) {
    Test * test = new Test;
    shared_ptr<Test> ptr1(test);
    shared_ptr<Test> ptr2(test);
}

运行结果:

Test start...
Test end...
Test end...

进程已结束,退出代码-1073740940 (0xC0000374)

为什么程序会崩溃?因为一个指针被毫不相关的两个shared_ptr托管,它们之间没有拷贝、赋值等关系,所以会各自引用计数。ptr1析构的时候,看到自身引用计数是1,减为零,然后delete test。ptr2析构的时候,仍然看到自身引用计数是1,再次delete test。注意到第二次test也不是空指针,是一个正常的地址,但是指向的对象已经被delete了,不能再次delete,否则就会引发程序崩溃。
根本原因,shared_ptr的引用计数由智能指针管理,正常应该由堆上的对象管理。
那该怎么办呢?最好的办法,如第一章所讲,使用make_shared方法初始化智能指针对象。

int main(void) {
    shared_ptr<Test> ptr1 = make_shared<Test>();
    shared_ptr<Test> ptr2 = make_shared<Test>();
}

简单地说,就是不要手动管理指针,这样一来就会避免一个指针被托管给多个毫不相关的shared_ptr。

你可能感兴趣的:(c++,java,开发语言)