当前位置 : 主页 > 编程语言 > c++ >

C++ 智能指针 shared_ptr 分析

来源:互联网 收集:自由互联 发布时间:2021-06-23
引文: C++对指针的管理提供了两种解决问题的思路: 1.不允许多个对象管理一个指针 2.允许多个对象管理一个指针,但仅当管理这个指针的最后一个对象析构时才调用delete ps:这两种思路的

引文:

C++对指针的管理提供了两种解决问题的思路:

1.不允许多个对象管理一个指针

2.允许多个对象管理一个指针,但仅当管理这个指针的最后一个对象析构时才调用delete

ps:这两种思路的共同点就是只允许delete一次,下面将讨论的shared_ptr就是采用思路1实现的

ps:智能指针不是指针,而是类,可以实例化为一个对象,来管理裸指针


1.shared_ptr的实现原理:

shared_ptr最本质的功能:“当多个shared_ptr管理同一个指针,仅当最后一个shared_ptr析构时,指针才被delete”,该功能是通过引用计数法实现的

引用计数法的规则:

  1)所有管理同一个裸指针的shared_ptr,都共享一个引用计数器

  2)每当一个shared_ptr被赋值给其他shared_ptr时,这个共享的引用计数器就加1

  3)每当一个shared_ptr析构或被用于管理其他裸指针时,这个引用计数器就减1

  4)如果此时发现引用计数器为0,那么说明它是管理这个指针的最后一个shared_ptr了,于是我们释放指针指向的资源

引用计数法的内部实现:

  1)这个引用计数器保存在某个内部类型中,而这个内部类型对象在shared_ptr第一次构造时以指针的形式保存在shared_ptr中

  2)shared_ptr重载了赋值运算符,在赋值和拷贝另一个shared_ptr时,这个指针被另一个shared_ptr共享

  3)在引用计数归0时,这个内部类型指针与shared_ptr管理的资源一起释放

  4)此外,为了保证线程安全,引用计数器的加1和减1都是原子操作,它保证了shared_ptr由多个线程共享时不会爆掉


2.shared_ptr的使用

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

int main()
{
    //初始化 方法1:
    shared_ptr<string> sptr1(new string("name"));
    //初始化 方法2:
    shared_ptr<string> sptr2=make_shared<string>("sex");
    //初始化 方法3:
    int *p =new int(10);
    shared_ptr<int> sptr3(p); //这种初始化的方式很危险,delete p之后,strp3也不再有效
}

相关成员函数:

1)use_count:返回引用计数的个数

2)unique:返回是否独占所有权(use_count=1)

3)swap:交换两个share_ptr对象(即交换所拥有的对象)

4)reset:放弃内部对象的所有权或拥有对象的变更,会引起原有对象引用计数的减少

5)get:返回内部对象指针


3.引用计数最大的缺点:循环引用

下面是事故现场:

class Observer; // 前向声明
class Subject
{
private:

    std::vector<shared_ptr<Observer>> observers;
public:
    Subject() {}
    addObserver(shared_ptr<Observer> ob)
    {
        observers.push_back(ob);
    }
    // 其它代码
};

class Observer
{
private:
    shared_ptr<Subject> object;
public:
    Observer(shared_ptr<Object> obj) : object(obj) {}
    
    // 其它代码
};

目标类subject连接这多个观察者类,当某个事件发生时,目标类可以遍历观察者数组observers,对观察者进行通知,而观察者类中也保留着目标类的shared_ptr,这样多个观察者之间可以以目标类为桥梁进行沟通,除了会发生内存泄漏外,这还是一种很不错的设计模式嘛……

这里产生内存泄漏的原因就是循环引用,循环引用指的是一个引用通过一系列的引用链,竟然引回到自身,在上面的例子中,subject->observer->subject就是这么一条环形引用链,假设我们程序中只有一个变量shared_ptr<sbuject> p,此时p指向的对象不仅通过shared_ptr引向自己,还通过它包含的observer中的object成员变量引回自己,于是它的引用计数是2,每个observer的引用计数都是1,当p析构时,它的引用计数2-1=1,大于0,其析构函数不会被调用,于是p和它包含的每个observer对象在程序结束时依然驻留在内存中,没有被delete,从而造成了内存泄漏


4.采用weak_ptr(弱引用)解决循环引用的问题:

标准库提供了std::weak_ptr,weak_ptr是shared_ptr的观察者,它与一个shared_ptr绑定,但是却不参与引用计数的计算,在需要时,它还能生成一个与它所观察的shared_ptr共享引用计数器的新的shared_ptr,总而言之,weak_ptr的作用就是:在需要时生成一个与绑定的shared_ptr共享引用计数器的新shared_ptr,在其他时候不干扰绑定的shared_ptr的引用计数

weak_ptr相关成员函数:

1)lock:获得一个和绑定的shared_ptr共享引用计数器的新的shared_ptr

2)expired:功能等价于判断use_count是否等于0,但是速度更快

继续引用上面subject和observer的例子,来解决循环引用的问题:

将上述例子中,observer中object成员的类型换成weak_ptr<subject>即可解决内存泄漏的问题,因为之前的observer中object成员的subject参与了引用计数,替换成weak_ptr<subject>之后没有参与引用计数,这样以来,p指向对象的引用计数为1,所以在p析构时,subject指针将被delete,其中包含的observer数组在析构时,内部的observer对象的引用计数也为0,所以他们也被deleete了,不存在内存泄漏的问题了

class Observer; // 前向声明
class Subject
{
private:

    std::vector<shared_ptr<Observer>> observers;
public:
    Subject() {}
    addObserver(shared_ptr<Observer> ob)
    {
        observers.push_back(ob);
    }
    // 其它代码
};

class Observer
{
private:
    shared_ptr< weak_ptr<Subject> > object;
public:
    Observer(shared_ptr<Object> obj) : object(obj) {}
    
    // 其它代码
};


5.错误用法1:多个无关的shared_ptr管理同一个裸指针,有可能导致二次析构

int main()
{
    int *a = new int(10);

    shared_ptr<int> p1(a);

    shared_ptr<int> p2(a);
}

p1和p2管理同一个裸指针a,此时的p1和p2有着完全独立的两个引用计数器,所以p1析构的时候会将a析构一次,p2析构的时候也会将a析构一次,C++中不允许同一个东西被析构两次,这样会导致程序爆炸

为了避免这种情况,我们永远不要将new用在shared_ptr构造函数列表以外的地方,或者干脆不用new,改用make_shared

另外,即使这样,也有可能导致二次析构,比如我们采用shared_ptr的get函数获得原始裸指针来构造另一个shared_ptr

class A
{
public:
    std::shared_ptr<A> getShared()
    {
        return std::shared_ptr<A>(this);
    }
};

int main()
{
    std::shared_ptr<A> pa = std::make_shared<A>();
    std::shared_ptr<A> pbad = pa->getShared();
}

上面的样例中,pa和pbad各自拥有一个独立的引用计数器,也有可能会导致二次析构

总而言之:管理同一个资源的sahred_ptr,只能由同一个初始shared_ptr通过一系列赋值和拷贝构造得到,要确保其共享的是同一个引用计数器


6.错误用法2:直接用new构造多个shared_ptr作为实参,可能会导致内存泄漏

// 声明
void f(A *p1, B *p2);

// 使用
f(new A, new B);

上面的代码很容易发生内存泄漏,假如new A先发生于new B,那么如果new B抛出异常,那么new A的分配将会发生泄漏

如果按照这种方式new多个share_ptr作为实参,依然会发生内存泄漏

//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(shared_ptr<A> (new A),shared_ptr<B>(new B));

因为shared_ptr的构造有可能发生在new A和new B之后,这里涉及到C++操作的sequence after性质,该性质保证:

1)new A发生在shared_ptr<A>构造发生之前

2)new B发生在shared_ptr<B>构造发生之前

3)两个shared_ptr的构造发生在函数f的调用之前

在满足上面三条性质的前提下,各操作的顺序可以任意执行

若不使用new而是使用make_shared来构造shared_ptr,那么就不会产生内存泄漏

//声明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(make_shared<A>(),make_shared<B>());

原因很简单,依然是sequence after性质,如果两个函数的执行顺序不确定,那么当一个函数执行时,另外一个函数不会执行,于是make_shared<A>的构造完成了,即使make_shared<B>的构造抛出了异常,那么A的资源也能够被正确的释放,和上面的情形相比较,make_shared保证了第二个new发生的时候,第一个new所分配的资源已经被shared_ptr管理起来了,所以在异常发生时,能够正确的释放资源

总结:请总是使用make_shared来生成shared_ptr


7.如果希望使用shared_ptr来管理动态数组,那么需要提供一个自定义的删除器来代替delete

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

class DelTest
{
public:
    DelTest(){
        j= 0;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
    static int i,j;
};

int DelTest::i = 0;
int DelTest::j = 0;

void noDefine()
{
    cout<<"no_define start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10]);

}

void slefDefine()
{
    cout<<"slefDefine start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
}

int main()
{
    noDefine();//!构造10次,析构1次。内存泄漏。
    cout<<"--------------------"<<endl;
    slefDefine();//!构造次数==析构次数 无内存泄漏
}
/*
运行结果:
no_define start running!
 DelTest():0
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
 DelTest():10
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/

需要注意的是:虽然通过自定义删除器的方式shared_ptr可以管理动态数组,但是shared_ptr并不支持下标运算符的操作,而且只能指针类型不支持指针算术运算(不能取地址),因此为了访问数组中的元素,必须用get获得一个原始内置裸指针,然后用它来访问数组元素

样例如下:

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

class DelTest
{
public:
    DelTest(){
        j= 0;
        x=i;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
    static int i,j;
    int x;
};

int DelTest::i = 0;
int DelTest::j = 0;

void noDefine()
{
    cout<<"no_define start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10]);

}

void slefDefine()
{
    cout<<"slefDefine start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!传入lambada表达式代替delete操作。
    cout<<p.get()[4].x<<endl;

}

int main()
{
    noDefine();//!构造10次,析构1次。内存泄漏。
    cout<<"--------------------"<<endl;
    slefDefine();//!构造次数==析构次数 无内存泄漏
}
/*
运行结果:
no_define start running!
 DelTest():0
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
 DelTest():10
5
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/


8.使用shared_ptr管理非常规的动态对象的时候,记得自定义删除器

某些情况下,有些动态内存也不是我们new出来的,如果要使用shared_ptr管理这种动态内存,也要自定义删除器

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

void closePf(FILE * pf)//即可以避免异常发生后无法释放内存的问题,也避免了很多人忘记执行fclose
{
    cout<<"----close pf after works!----"<<endl;
    fclose(pf);
}

int main()
{
    shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);
    cout<<"*****start working****"<<endl;
    if(!pf)
        return -1;
    char *buf = "abcdefg";
    fwrite(buf,8,1,pf.get());//确保fwrite不会删除指针的情况下,可以将shared_ptr内置指针取出
    cout<<"------write in file!-----"<<endl;
}
/*
*****start working****
------write in file!-----
----close pf after works!----
*/

类比TCP/IP中连接打开和关闭的情况,同理都可以使用shared_ptr来管理


总结:

1)不用使用相同的内置/原始/裸指针初始化多个智能指针

2)不要delete get函数返回的指针

3)如果你使用了get返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了

4)如果你使用的智能指针管理的资源不是new分配的内存,记得传递一个删除器

5)请勿使用new构造多个shared_ptr作为实参,应该使用make_shared

6)存在循环引用关系时,请使用weak_ptr来保证不会产生内存泄漏

网友评论