• C++智能指针


    C++智能指针

    来源 https://zhuanlan.zhihu.com/p/30933682

    参考 https://www.zhihu.com/question/319277442/answer/1094961099

    ========================

    智能指针只能代替T*的一部分功能,而这部分本来就不适合用T*(因为容易造成bug)。

    如果本身就没有所有权,Bjarne(P1408)的建议是直接用T*

    ========================

    智能指针表示的是某个资源的“所有权”的概念,

    unique_ptr表示唯一的所有权;

    shared_ptr表示可共享的所有权;

    weak_ptr表示可共享的未来的所有权。

    然而资源拥有者如果只是要把一个对象借给别人用一下,用完就归还呢?

    void borrow_sth(??? resource);

    有两种选择: T*const std::unique_ptr<T>&

    我更喜欢用第一种

    ========================

    嗯,大家都在说普通指针和智能指针,少说了一个“引用”

    先考虑用引用和值,减少90%的指针,特别是函数之间,基本上没有必要传指针。

    再考虑使用unique_ptr,替换类的成员不能用引用的情况下使用指针。

    最后考虑使用share_ptr/weak_ptr,解决生命周期复杂到一个类管不了的情况。

    最后才是合理的指针使用场景。

    ========================

    明确资源所归属的类,用unique_ptr的容器作为类成员变量+移动语义+RAII管理资源,其他地方裸指针访问资源,这样能把c++写出java的感觉,也不用担心pointer invalidation之类的问题。

    shared_ptr用的很少,资源归属不明确的情况下先看看是否代码结构设计有问题,实在不行再用shared_ptr。

    ========================

    滥用智能指针会把你的代码污染.

    • 确定持有者 :使用unique_ptr
    • 不需要持有对象 or 解决互相强引用 :使用waek_ptr
    • 需要共享时 :使用shared
    • 无主对象,与C交互,传递可能为空的对象,传递需要修改的对象:指针
    • 传递不能为nullptr的对象:引用,const 引用

    综上:

    你代码是这样写,没问题,但是历史代码,或者C代码,别人的代码,可不会这么让你用的 随心所欲.

    ========================

    1 两种指针的天赋各不相同

    a 智能指针天生负责对象生命期管理(这里假设智能指针作为类的非静态成员变量,并借助类的构造函数和析构函数来完成动态对象的自动化管理):所以动态对象的创建和析构全都由unique_ptr和shared_ptr来做。

    b 原始指针天生不负责对象生命周期管理:原始指针擅长调用动态对象,原因就是简化接口。

    2 创建者与使用者

    c 所以你看MFC,BCG,QT,操作系统API,在结合业务数据尤其是动态对象的时候都是原始指针。这样做的好处是它明确告诉你它不管理你的动态对象,只负责使用!只负责使用!只负责使用!并暗示你,用之前你要创建好,用完了它不负责清理。

    d 上面c主要讲了原始指针作为动态对象使用者的场景。这就再次暗示我们,在类内部我们使用智能指针来管理动态对象,表示动态对象的拥有者;在类外部我们提供原始指针接口供动态对象的使用者使用;

    e 补充一下d,全局函数和类的静态函数肯定可以看做是类的外部。因为我们之所以使用智能指针可以自动化管理就是利用了类的构造函数和析构函数,而全局函数和类的静态函数显然利用不了。

    ~~~~~~~~~~~更新5/11~~~~~~

    f 补充一下d,有些业务场景,比如流水线处理的,生产者消费者模式下,用原始指针比较简单明了,谁使用谁释放。这时候没必要再用智能指针了,因为很难做到自动化释放。

    下面是示例代码:

    class T3;
    class T2;
    class T1;
     
    class A
    {
        T3* m_t2;//创建和释放都由A之外的代码管理,A只负责(借用)使用;业务逻辑上保证A在使用m_t2指向的对象的时候,对象始终是存在的
        shared_ptr<T2> m_t2;//由加载程序创建m_t2指向的对象,执行时,交给A来管理,涉及动态对象的管理权的交接
        unique_ptr<T1> m_t1;//A管理该对象的生命周期,A的构造函数构造m_t1, A的析构函数释放m_t1;
    };

    ========================

    推荐看 Effective Modern C++;大师的著作真乃实用之王。

    我摘录一下:

    std::unique_ptr:

    1 小巧、高速、具备只移型别的智能指针,对托管资源实施专属所有权语义。

    2 默认地,资源析构采用delete运算符来实现,但可以指定自定义删除器。有状态的删除器和采用函数指针实现的删除器会增加std::unique_ptr型别的对象尺寸

    3 将std::unique_ptr 转换成std::shared_ptr是容易实现的

    std::shared_ptr:

    1 提供方便的手段,实现了任意资源在共享所有权语义下进行生命周期管理的垃圾回收

    2 与std::unique_ptr 相比,std::shared_ptr的尺寸通常是裸指针尺寸的两倍,它还会带来控制块的开销,并要求原子化的引用计数操作

    3 默认的资源析构通过delete运算符进行,但同时也支持定制删除器。删除器的型别对std::shared_ptr的型别没有影响

    4 避免使用裸指针型别的变量来创建 std::shared_ptr 指针

    std::weak_ptr:

    1 使用std::weakptr 来代替可能空悬的std::shared_ptr

    2 std::weak_ptr 可能的用武之地包括缓存,观察者列表,以及避免std::shared_ptr 指针环路。

    看看大师的总结,完美!!

    具体到完全代替裸指针,恐怕也要再掂量掂量。

    下面写一些劣势

    std::shared_ptr 会增加内存开销,复制的时候cpu消耗提高【原子count操作】

    std::unique_ptr 内存占用小,几乎可以媲美裸指针;但是它毕竟是一个类,使用的时候,不能复制,导致你一个作用域内只能有一个可用的实例【类似rust的所有权吧,你用起来有点束手束脚】;

    std::weak_ptr 必须跟std::shared_ptr配合使用。

    优势:

    省去你自己判断啥时候该释放资源【异步回调时候智能指针可以完美避免手动控制生命周期;enable_shared_frome_this 已经可以算是一种特别的编程技巧了

    媲美裸指针的操作习惯

    解放了双手,C++跟脚本的代码越来越像了。

    补充一下【之前审题不太好】

    1.对于性能和内存使用有严格要求的场景,不要过于依赖智能指针。【比如嵌入式这些的,实际上C+class就够了】

    2.对于类型不敏感的数据【也就是内存了】,可以考虑使用std::array或者std::vector等。因为这个时候,你实际上就是C的操作,类型对于该内存仅仅起到一个布局描述的作用,频繁的类型转换【非继承关系】、字段偏移等操作,用智能指针也没有什么好处【因为你还是会拿到裸指针去操作】

    3.其他的对类型敏感,或者对作用域敏感的数据内存,可以都考虑使用智能指针。局部作用域使用uniqe_ptr , 多作用域的使用shared_ptr,缓存可能失效的情况下使用weak_ptr。

    我做一般应用的时候,除了容器,几乎一上来全部使用uniqe_ptr,当需要抛出一个副本的时候,使用shared_ptr。当功能完成的时候,哪个内存是全局生命周期,改成裸指针【全局裸指针我都不判空】。如果该项目不是那么重要,甚至我都会全部用shared_ptr,不用关心性能问题,因为C++本身的编译性能已经很高了,随便写写性能就够了,只要不飞,内存泄漏不是问题。

    当我要去判断某一个内存之后的操作会失效,但是不知道什么时候失效的时候,我使用weak_ptr和shared_ptr。通过weak_ptr接口可以线程安全的获取我之前的智能指针是否还生效。【这个时候,裸指针,几乎是没有办法的了,很容易出现野指针】

    ========================

    早在1994年,Gregory Colvin就向C++标准委员会提出了智能指针的提案(Smart Pointers - 1.54.0)。但早期的设计并不好用。各个库都有自己的一套智能指针,没有标准化。经过20多年的发展,特别是C++11标准引入shared_ptr和unique_ptr之后,智能指针技术趋于成熟。然而在实践中,大多数项目还在使用自己山寨的引用计数解决方案。智能指针还没有成为C++程序员的常备技能。

    像其他技术一样,智能指针有一定学习成本,如果被误用,同样也会带来各种bug。本文简要回顾C++11智能指针shared_ptr/unique_ptr/weak_ptr的核心概念,并试图总结其正确使用方法。

    Ownership Logic

    正确使用智能指针的前提是搞清楚业务逻辑需要。其中最重要的是设计资源管理,即ownership,并据此选择是否使用智能指针,使用哪种智能指针。智能指针有其内在的ownership logic。

    所谓own某个指针,意味着有责任在合适的时候释放该指针。获得、引用和使用某个指针,并不一定需要负责释放该指针所指向的资源。

    shared_ptr是shared ownership,owner发起释放操作,只是减引用计数,只有所有owner都释放,所指向的对象才真正释放。

    weak_ptr不控制对象的生命周期,但是它观察着shared_ptr管理的对象,有办法知道对象是否还活着。

    unique_ptr则是unique ownership,对象的管理权可以转移,但是同一时刻只有一个owner,否则编译就会报错。

    shared_ptr, weak_ptr

    shared_ptr在底层使用了两个技术,一个是引用计数,另一个是引入了一个中间层(Be Smart About C++11 Smart Pointers)。

    为了管理目标对象,所创建的中间层被称为manager object。其中除了目标对象的裸指针,还有两个引用计数。一个用于shared_ptr,一个用于weak_ptr。当shared count减到0的时候,managed object就会被销毁。只有shared count和weak count都减到0的时候,manager object才会被销毁。

    enable_shared_from_this

    如果涉及到将this指针提升为shared_ptr的情况,直接提升会新建一个manager object。

    void f(shared_ptr<Thing>);
    class Thing {
    public:
        void foo() {
            //f(shared_ptr<Thing>(this));   //new manager object A
            f(shared_from_this());  //use manager object B
        }
    };
    int main() {
        shared_ptr<Thing> sp(new Thing());  //new manager object B
        sp->foo();
    }
    

    使用两个manager object管理同一个对象会造成不可预知的后果。为避免这种情况,需要在对象中维护一个weak_ptr。这是通过enable_shared_from_this自动完成的。

    当需要在object内部使用this指针时,调用shared_from_this()就可以避免新建manager object。需要注意的是,在构造函数中,对象还未构造完毕,并没有交由shared_ptr管理,即manager object还未创建,所以不能使用shared_from_this。

    unique_ptr

    unique_ptr是对裸指针的简单封装,不需要额外的manager object。和shared_ptr基本用法一致,只是unique_ptr没有引用计数,内部指针要么有效,要么没有。

    unique_ptr可以用(unique_ptr rvalue)初始化,但不允许copy construction或copy assignment。这符合其unique ownership语义。所以如果函数参数中有unique_ptr,应该传引用或指针,不能传值。

    unique_ptr<Thing> create() {
        unique_ptr<Thing> ptr(new Thing);
        return ptr; //rvalue
    }
    unique_ptr<Thing> ptr2(create());
    //unique_ptr<Thing> ptr3(ptr2);  //copy construction
    //unique_ptr<Thing> ptr3 = ptr2; //copy assignment
    

    Race Condition

    c++20将有atomic_shared_ptr/atomic_weak_ptr,这是不是意味着shared_ptr/weak_ptr并不是线程安全的呢?

    一般地讲,manager object中的引用计数增减是原子操作,所以是线程安全的。同时读shared_ptr也是安全的。但是如果有线程在读写同一个shared_ptr,就不是安全的(shared_ptr - 1.57.0),这和操作一般指针是一致的。如果两个线程在操作两个shared_ptr,即使他们指向同一个manager object,只要没有访问所管理的对象,就是安全的(Lesson #4: Smart Pointers)。

    shared_ptr<int> p(new int(42));
    
    // thread A
    shared_ptr<int> p2(p); // reads p
    p2.reset(new int(1912)); //writes p2
    
    // thread B
    shared_ptr<int> p3(p); // OK, multiple reads are safe
    p3.reset(); // OK, writes p3
    

    需要指出的是,从weak_ptr.lock()提升为shared_ptr是线程安全的。unique_ptr的所有权转让也是安全的。但是使用unique_ptr操作对象是不安全的(Atomic Smart Pointers)。

    智能指针正确用法

    使用shared_ptr/weak_ptr/unique_ptr必须要注意:

    1)必须保证所有managed object只有一个manager object

    当object第一次创建的时候,就要立刻交由一个shared_ptr管理,其他的shared_ptr和weak_ptr都必须从第一个shared_ptr拷贝或赋值得到。具体来说,就是调用shared_ptr(raw ptr)和make_shared函数。

    class Thing;
    shared_ptr<Thing> p1(new Thing);
    shared_ptr<Thing> p2(make_shared<Thing>());
    

    其中使用make_shared一次性分配managed object和manager object两块内存,效率更高。而且对于强迫症来说,看到new看不到delete总是感觉挺难受的,还不如连new都不要看到。

    2)能用裸指针解决问题的情况下,就不要使用智能指针。
    如果决定了用智能指针,那就不要用裸指针管理同一个对象。

    虽然通过智能指针get()函数可以得到裸指针,但是尽量不要用。对象一经创建,就应该纳入shared_ptr/unique_ptr的管理之下。为了保证没有裸指针暴露出来,应该只用shared_ptr(raw ptr)/unique_ptr(raw_ptr)和make_shared/make_unique函数创建对象。

    3)ownership logic是正确使用智能指针的基础,不要滥用shared_ptr

    weak_ptr是为了解决循环引用而引入的。当系统中出现了循环引用,且都是使用shared_ptr管理对象,那么一定是shared_ptr被滥用了。

    weak_ptr观察着shared_ptr管理的对象,必须从shared_ptr或weak_ptr创建。其唯一正确的使用方法是先用lock()调用提升为shared_ptr,然后使用shared_ptr。如果直接用shared_ptr(weak_ptr)来构造,可能会在weak_ptr已经expire的情况下抛出异常。

    shared_ptr可以用==,!=,<来比较,实际比较的是他们管理的裸指针。

    shared_ptr有拷贝开销,作为参数时,应该尽量用const reference。

    4) 能用unique_ptr管理的对象,不要使用shared_ptr/weak_ptr

    这其实是说尽量在单一的固定地方管理资源,如果不能保证ownership固定,可以转移所有权,尽量保证只有一个owner(An overview on smart pointersGoogle C++ Style GuideTop 10 dumb mistakes to avoid with C++ 11 smart pointers - A CODER'S JOURNEY)。

    最佳实践实例

    1)main thread拥有对象,加载子线程只管加载。

    //main thread
    class Thing;
    shared_ptr<Thing> p1(make_shared<Thing>());
    if (p1->getState() == eLoadFinish) ... //use after loading finish
    
    //loading thread
    weak_ptr<Thing> p2(p1);
    shared_ptr<Thing> p3 = p2.lock();
    if (p3 && p3->getState() != eLoadFinish)
    {
        ... //loading
        p3->setState(eLoadFinish); //set loading finish flag
    }
    

    此处不宜使用unique_ptr。虽然在子线程的加载过程中可以上锁,但是对象中途若被主线程释放,将会宕机。

    2)拥有者和使用者都在main thread,使用者需要定期对对象做操作。

    class Thing;
    void funcUsePtr(const shared_ptr<Thing> &p){
        p->xxx(); //method call
    }
    void funcPassToUser(shared_ptr<Thing> &p){
        pUser = p;
    }
    
    //owner
    shared_ptr<Thing> p1(make_shared<Thing>());  
    funcUsePtr(p1); //normal use
    funcPassToUser(p1);  //pass to pUser
    
    //user
    shared_ptr<Thing> p2 = pUser.lock();
    if (p2)
        p2->yyy(); //method call
    

    此处虽然只有一个线程,但也不宜用unique_ptr。试想owner如果中途要释放对象,user是不知道的。此时用weak_ptr维持一个弱引用,当需要的时候检查一下有效性,是比较合理的。

    ============== End

  • 相关阅读:
    vue this触发事件
    jQuery获取地址栏中的链接参数
    vue 省市区三级联动
    图片文字css小知识点
    sticky footer 模板
    Django学习——用户自定义models问题解决
    Django学习——全局templates引用的问题
    Django的学习——全局的static和templates的使用
    selenium登录爬取知乎出现:请求异常请升级客户端后重试的问题(用Python中的selenium接管chrome)
    使用python远程连接数据库
  • 原文地址:https://www.cnblogs.com/lsgxeva/p/12734728.html
Copyright © 2020-2023  润新知