zoukankan      html  css  js  c++  java
  • [C++11新特性] weak_ptr和unique_ptr

    一、weak_ptr弱引用的智能指针

    1.1 shared_ptr相互引用会有什么后果?

    shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。看下面的例子:

    #include <iostream>
    #include <memory>
    
    class Parent;  // Parent类的前置声明
    
    class Child {
    public:
        Child() { std::cout << "hello child" << std::endl; }
        ~Child() { std::cout << "bye child" << std::endl; }
    
        std::shared_ptr<Parent> father;
    };
    
    class Parent {
    public:
        Parent() { std::cout << "hello Parent" << std::endl; }
        ~Parent() { std::cout << "bye parent" << std::endl; }
    
        std::shared_ptr<Child> son;
    };
    
    void testParentAndChild() {
    
    }
    
    int main() {
        std::shared_ptr<Parent> parent(new Parent());  // 1  资源A
        std::shared_ptr<Child> child(new Child());  // 2   资源B
        parent->son = child;     // 3   child.use_count() == 2 and parent.use_count() == 1 
        child->father = parent;  // 4   child.use_count() == 2 and parent.use_count() == 2
    
        return 0;
    }
    
    /*
    输出:
    hello Parent
    hello child
    */
    

    很惊讶的发现,用了shared_ptr管理资源,没有调用 Parent 和 Child 的析构函数,表示资源最后还是没有释放!内存泄漏还是发生了。

    分析:

    • 执行编号1的语句时,构造了一个共享智能指针p,称呼它管理的资源叫做资源Anew Parent()产生的对象)吧, 语句2构造了一个共享智能指针c,管理资源B(new Child()产生的对象),此时资源AB的引用计数都是1,因为只有1个智能指针管理它们,执行到了语句3的时候,是一个智能指针的赋值操作,资源B的引用计数变为了2,同理,执行完语句4,资源A的引用计数也变成了2
    • 出了函数作用域时,由于析构和构造的顺序是相反的,会先析构共享智能指针c,资源B的引用计数就变成了1;接下来继续析构共享智能指针p,资源A的引用计数也变成了1。由于资源AB的引用计数都不为1,说明还有共享智能指针在使用着它们,所以不会调用资源的析构函数!
    • 这种情况就是个死循环,如果资源A的引用计数想变成0,则必须资源B先析构掉(从而析构掉内部管理资源A的共享智能指针),资源B的引用计数想变为0,又得依赖资源A的析构,这样就陷入了一个死循环。

    1.2 weak_ptr如何解决相互引用的问题

    要想解决上面循环引用的问题,只能引入新的智能指针std::weak_ptrstd::weak_ptr有什么特点呢?与std::shared_ptr最大的差别是在赋值的时候,不会引起智能指针计数增加。

    • weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
    • 同样,在weak_ptr析构时也不会导致引用计数的减少,它只是一个静静地观察者。weak_ptr没有重载operator*->,这是特意的,因为它不共享指针,不能操作资源,这是它弱的原因。
    • 如要操作资源,则必须使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。

    当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它:

    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp弱共享p; p的引用计数未改变
    

    我们在上面的代码基础上使用std::weak_ptr进行修改,如下:

    #include <iostream>
    #include <memory>
    
    class Parent;  // Parent类的前置声明
    
    class Child {
    public:
        Child() { std::cout << "hello child" << std::endl; }
        ~Child() { std::cout << "bye child" << std::endl; }
    
        // 测试函数
        void testWork()
        {
            std::cout << "testWork()" << std::endl;
        }
    
        std::weak_ptr<Parent> father;
    };
    
    class Parent {
    public:
        Parent() { std::cout << "hello Parent" << std::endl; }
        ~Parent() { std::cout << "bye parent" << std::endl; }
    
        std::weak_ptr<Child> son;
    };
    
    void testParentAndChild() {
    
    }
    
    int main() {
        std::shared_ptr<Parent> parent(new Parent());
        std::shared_ptr<Child> child(new Child());
        parent->son = child;
        child->father = parent;
        std::cout << "parent_ref:" << parent.use_count() << std::endl;
        std::cout << "child_ref:" << child.use_count() << std::endl;
    
        // 把std::weak_ptr类型转换成std::shared_ptr类型,以调用内部成员函数
        std::shared_ptr<Child> tmp = parent.get()->son.lock();
        tmp->testWork();
        std::cout << "tmp_ref:" << tmp.use_count() << std::endl;
    
        return 0;
    }
    
    /*
    输出:
    hello Parent
    hello child
    parent_ref:1
    child_ref:1
    testWork()
    tmp_ref:2
    bye child
    bye parent
    */
    

    由以上代码运行结果我们可以看到:

    • 所有的对象最后都能正常释放,不会存在上一个例子中的内存没有释放的问题;
    • parent 和 child 在 main 函数中退出前,引用计数均为 1,也就是说,对std::weak_ptr的相互引用,不会导致计数的增加。

    1.3 weak_ptr常用操作

    weak_ptr<T> w;	// 空weak_ptr可以指向类型为T的对象
    weak_ptr<T> w(shared_ptr p);	// 与p指向相同对象的weak_ptr, T必须能转换为sp指向的类型
    w = p;	// p可以是shared_ptr或者weak_ptr,赋值后w和p共享对象
    w.reset();	// weak_ptr置为空
    w.use_count();	// 与w共享对象的shared_ptr的计数
    w.expired();	// w.use_count()为0则返回true,否则返回false
    w.lock();	// w.expired()为true,返回空的shared_ptr;否则返回指向w的shared_ptr
    

    二、unique_ptr独占的智能指针

    2.1 unique_ptr的基本使用

    unique_ptr相对于其他两个智能指针更加简单,它和shared_ptr使用差不多,但是功能更为单一,它是一个独占型的智能指针,不允许其他的智能指针共享其内部的指针,更像原生的指针(但更为安全,能够自己释放内存)。不允许赋值和拷贝操作,只能够移动

    std::unique_ptr<int> ptr1(new int(0));
    std::unique_ptr<int> ptr2 = ptr1; // 错误,不能复制
    std::unique_ptr<int> ptr3 = std::move(ptr1); // 可以移动
    

    在 C++11 中,没有类似std::make_shared的初始化方法,但是在 C++14 中,对于std::unique_ptr引入了std::make_unique方法进行初始化。

    #include <iostream>
    #include <memory>
    
    int main()
    {
        std::unique_ptr<std::string> ptr1(new std::string("unique_ptr"));
        std::cout << "ptr1 is " << *ptr1 << std::endl;
    
        std::unique_ptr<std::string> ptr2 = std::make_unique<std::string>("make_unique init!");
        std::cout << "ptr2 is " << *ptr2 << std::endl;
    
        return 0;
    }
    /*
    输出:
    ptr1 is unique_ptr
    ptr2 is make_unique init!
    */
    

    2.2 unique_ptr常用操作

    下面列出了unique_ptr特有的操作。

    unique_ptr<T> u1 // 空unique_ptr,可以指向类型为T的对象。u1会使用delete来释放它的指针
    unique_ptr<T, D> u2 // u2会使用一个类型为D的可调用对象来释放它的指针
    unique_ptr<T, D> u(d) // 空unique_ptr,指向类型为T的对象,用类型为D的对象d替代delete
    u = nullptr // 释放u指向的对象,将u置为空
    u.release() // u放弃对指针的控制权,返回指针,并将u置为空
    u.reset() // 释放u指向的对象
    u.reset(q) // 如果提供了内置指针q,另u指向这个对象;否则将u置为空
    u.reset(nullptr)   
    

    虽然我们不能拷贝或赋值unique_ptr,但可以通过调用 release 或 reset 将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr

    unique_ptr<string> p1(new string("Stegosaurus"));
    // 将所有权从pl (指向string Stegosaurus)转移给p2 
    unique_ptr<string> p2(p1, release()); // release 将 p1 置为空 
    unique_ptr<string> p3(new string("Trex"));
    
    // 将所有权从p3转移给p2
    p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
    

    调用 release 会切断unique_ptr和它原来管理的对象间的联系,如果我们不用另一个智能指针来保存 release 返回的指针,我们的程序就要负责资源的释放:

    p2.release(); // 错误:p2不会释放内存,而且我们丢失了指针
    auto p = p2.release(); // 正确,但我们必须记得 delete(p)
    delete(p);
    

    2.3 传递unique_ptr参数和返回unique_ptr

    不能拷贝 unique_ptr 的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的 unique_ptr。最常见的例子是从函数返回一个unique_ptr

    unique_ptr<int> clone (int p) 
    {
    	unique_ptr<int> ret(new int (p));
    	// ...
        return ret;
    }
    

    对于上面这段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,在《C++ Primer》13.6.2节(第473页)中有介绍。

    三、性能与安全的权衡

    使用智能指针虽然能够解决内存泄漏问题,但是也付出了一定的代价。以shared_ptr举例:

    • shared_ptr的大小是原始指针的两倍,因为它的内部有一个原始指针指向资源,同时有个指针指向引用计数。
    • 引用计数的内存必须动态分配。虽然一点可以使用make_shared()来避免,但也存在一些情况下不能够使用make_shared()
    • 增加和减小引用计数必须是原子操作,因为可能会有读写操作在不同的线程中同时发生。比如在一个线程里有一个指向一块资源的shared_ptr可能调用了析构(因此所指向的资源的引用计数减一),同时,在另一线程里,指向相同对象的一个shared_ptr可能执行了拷贝操作(因此,引用计数加一)。原子操作一般会比非原子操作慢。但是为了线程安全,又不得不这么做,这就给单线程使用环境带来了不必要的困扰。

    我觉得还是分场合吧,看应用场景来进行权衡,我也没啥经验,但我感觉安全更重要,现在硬件已经足够快了,其他例如java这种支持垃圾回收的语言不还是用的很好吗。


    参考:

    《C++ Primer 第5版》

    c++11&14-智能指针专题

    c++11]智能指针学习笔记


  • 相关阅读:
    Gradle 修改 Maven 仓库地址
    返回到上一页的html代码的几种写法
    Android网络传输中必用的两个加密算法:MD5 和 RSA (附java完成测试代码)
    MyBatis传入多个参数的问题
    8 个最优秀的 Android Studio 插件
    Html中隐藏a标签
    js中获取jsp中的参数
    同一个页面里的JS怎样获取jsp从别的页面获取的参数
    《Android源码设计模式解析与实战》读书笔记(一)
    HDU ACM 1068 最大独立集
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14576078.html
Copyright © 2011-2022 走看看