shared_ptr的理解和注意事项

昨天把shared_ptr的源码分析了一遍,也看了一些有关shared_ptr分析的文章,今天自己来总结一下。

以独立语句将newed对象置入智能指针

对于下面这样一个使用智能指针的函数调用,可能会造成内存泄漏:

process(std::shared_ptr<int>(new int(10)), priority());

编译器没有规定传入参数的调用顺序,可能先执行new表达式,然后执行priority()函数,最后构造shared_ptr对象。如果priority()函数中抛出异常,那么new出来的内存将得不到释放。这会造成内存泄漏。

所以要使用下面两种方式:
方式一:

shared_ptr<int> p(new int(10));
process(p, priority());

方式二:

process(std::make_shared<int>(10), priority);

前者即是以独立语句将new分配过的对象置入只能指针,后者是利用工厂函数,直接生成只能指针。

小知识:make_shared()和普通shared_ptr(new xx)构造方法的区别

make_shared的效率更高。直接使用shared_ptr(new xx)底层实际上涉及两次内存分配,一次是为被管理的对象分配内存,一次是为ref_count控制块分配内存。而make_shared则是一次性分配好一大块内存来同时持有被管理的对象和ref_count控制块。增加了代码执行速度,且能避免一些控制块的薄记信息(内存cookie),潜在减少了程序占用的空间。不过make_shared有个缺点,使用make_shared会导致对象销毁和内存被回收之间可能会有延迟。因为使用make_shared相当于把被管理对象内存和ref_count控制块内存绑定在一起了,我们知道,ref_count控制块内存本来是由use_count和weak_count共同维护的,假设被管理对象引用计数为0了,但是仍然有weak_ptr存在,那么这一大块内存就不能销毁;而传统的shared_ptr(new xx)方式,当对象引用计数为0时,对象内存销毁。当weak_ptr引用计数为0时,ref_count控制块内存销毁,所有的内存都能够及时清理。

  • 相比于使用new,make函数可以消除代码重复,提高异常安全。而且std::make_shared生成的代码更小更快。不适用make函数的场合包括自定义删除器和想要传递大括号初始值(这个暂时不太懂)。使用make函数不明智的场合包括:(1)自定义内存管理函数的类(2)内存紧张的系统,有非常大的对象,然后std::weak_ptr比std::shared_ptr长寿。

注意循环引用带来的内存泄漏

循环引用就是指,两个shared_ptr,你持有我,我持有你,双方都认为对方死了自己才能销毁,结果两方都无法销毁,造成内存泄漏。

示例代码:

class B;

class A { 
public:
    A(){
        std::cout<<"A ctor"<<std::endl;
    }   
    ~A(){
        std::cout<<"A dtor"<<std::endl;
    }   
    void do_something() {
        if(b_.lock()){
            std::cout<<"lock success, still alive"<<std::endl; 
            std::cout<<"use_count="<<b_.use_count()<<std::endl;  //输出1
        }   
    }   
public:
    //std::shared_ptr<B> b_;
    std::weak_ptr<B> b_; 
};

class B { 
public:
    B(){
        std::cout<<"B ctor"<<std::endl;
    }   
    ~B(){
        std::cout<<"B dtor"<<std::endl;
    }
public:
    std::shared_ptr<A> a_;
};

int main()
{
    std::shared_ptr<A> pa(new A);
    std::shared_ptr<B> pb(new B);
    std::cout<<"pb->use_count"<<pb.use_count()<<std::endl;
    pa->b_ = pb;
    pb->a_ = pa;
    pa->do_something();

    std::weak_ptr<B> pbb(pb);
    std::cout<<(pbb.lock()).use_count()<<std::endl; //输出2

    return 0;

我们可以使用weak_ptr来打破这个局面,而不是手动打破循环。一方持有另一方的shared_ptr(强引用),而另外一方则持有对方的weak_ptr(弱引用)。与之前的区别是,持有弱引用的一方在使用weak_ptr时,需要使用lock()方法,提升成为shared_ptr,提升失败说明对方已死,提升成功便可正常使用。这样就可以打破循环引用,避免内存泄漏。

代码中要注意的一点是,weak_ptr.lock()方法虽然会提升引用计数,但是如果我们仅仅把.lock()用作表达式,那么实际上是临时对象temp存在的时候引用计数为2,临时对象在表达式结束后立即死亡,引用计数又回到1。所以,我们如果要使用weak_ptr提升成为shared_ptr,必须用一个shared_ptr来接收,否则weak_ptr不会提升成为shared_ptr。

类向外传递this与shared_ptr

当我们在一个类的成员函数中,想知道自己this所在的shared_ptr时,不可以用shared_ptr(this)直接构造,因为这样会构造一个独立的局部shared_ptr,该shared_ptr销毁时会释放掉this。如果外部不知道该对象已经销毁,再调用就会爆炸。所以应该用shared_from_this,所在类需要继承std::enable_shared_from_this。

示例代码:

class A : public std::enable_shared_from_this<A> {  //注意继承要加模板参数
public:
    A() { std::cout<<"ctor"<<std::endl; }
    ~A() { std::cout<<"dtor"<<std::endl; }
public:
    void func() {
        //std::shared_ptr<A> local_sp(this);   //错误用法
        //std::cout<<local_sp.use_count()<<std::endl;  //引用计数为1,因为这个局部的shared_ptr是用this单独生成的,和外部没有关系,所以会造成两次销毁doble delete
        std::shared_ptr<A> local_sp = shared_from_this();   //正确用法
        std::cout<<local_sp.use_count()<<std::endl;  //引用计数为2
    }   
};

int main()
{
    std::shared_ptr<A> sp(new A); 
    {   
        sp->func();
    }   

    return 0;
}

上述错误情况输出是:

这里写图片描述

正确情况输出是:
这里写图片描述

可见,使用this在类内部再造一个shared_ptr是多么可怕的事情,会造成多次释放。

在多线程环境环境中,我们可能在成员函数中需要将this的智能指针回调给别的类对象,就可使用shared_from_this()。这样,别的对象就能够知道本对象是否还活着,避免在死亡之后的时候调用,产生可怕的后果。

shared_ptr的技术与陷阱

  1. 意外延长对象生命周期。可能容器持有shared_ptr,或者boost::function持有,这都会不经意间延长对象的生命期。我们可能会通过让他们持有weak_ptr来避免生命期被延长。
  2. 函数参数,一个线程只要在最外层持有一个实体,那么安全不成问题。比如:

    void on_message(const string& msg){
        shared_ptr<foo> f(new foo(msg));  //在最外层有一个实体,然后将shared_ptr传给谁都可以,因为始终有一个引用计数为1存在。常引用方式传递效率更高。
        if(validate(f)){   //const reference
            save(f);  //const reference
        }
    }
    
  3. 虚析构不再是必须。因为shared_ptr有T和Y两个参数。使用base类的shared_ptr接收derived的shared_ptr,ref_count中维护的指针类型仍是T,即base*类型。而shared_ptr类中维护的才是Y。删除器还是按老样子删除。所以不要虚析构也成。

有一张图可以看一下:

这里写图片描述

暂时完毕。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值