• 第23课 优先选用make系列函数


    一. make系列函数

    (一)三个make函数

      1. std::make_shared:用于创建shared_ptr。GCC编译器中,其内部是通过调用std::allocate_shared来实现的

      2. std::make_unique:C++14中加入标准库。

      3. std::allocate_shared:行为和std::make_shared一样,只不过第1个实参是个用以动态分配内存的分配器对象。

    //make_unique的模拟实现
    template<typename T, typename...Ts>
    std::unique_ptr<T> make_unique(Ts&&...params)
    {
        return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
    }
    
    //make_shared的实现(GCC编译器)
    template<typename _Tp, typename... _Args>
    inline shared_ptr<_Tp> make_shared(_Args&&... __args)
    {
        typedef typename std::remove_const<_Tp>::type _Tp_nc;
        return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                       std::forward<_Args>(__args)...);
    }
    std::make_unique和std::make_shared的实现

    (二)与new相比,make系列函数的优势

      1. 避免代码冗余:创建智能指针时,被创建对象的类型只需写1次。如make_shared<T>(),而用new创建智能指针时,需要写2次。

      2. 异常安全:make系列函数可编写异常安全代码,改进了new的异常安全性。

      3. 提升性能:编译器有机会利用更简洁的数据结构产生更小更快的代码。使用make_shared<T>时会一次性进行内存分配,该内存单块(single chunck)既保存了T对象又保存与其相关联的控制块。而直接使用new表达式,除了为T分配一次内存,还要为与其关联的控制块再进行一次内存分配。 

    二. make系列函数的局限

    (一)所有的make系列函数都不允许自定义删除器

    (二)make系列函数创建对象时,不能接受{}初始化列。(这是因为完美转发的转发函数是个模板函数,它利用模板类型进行推导。因此无法将“{}”推导为initializer_list,具体见《完美转发》一课)。换言之,make系列只能将圆括号内的形参完美转发。

    (三)自定义内存管理的类(如重载了operator new 和operator delete),不建议使用make_shared来创建。原因如下:

      1. 重载operator new和operator delete时,往往用来分配和释放该类精确尺寸(sizeof(T))的内存块。

      2. 而make_shared创建的shared_ptr,是一个自定义了分配器(std::allocate_shared)和删除器的智能指针,由allocate_shared分配的内存大小也不等于上述的尺寸,而是在此基础上加上控制块的大小。

      3. 因此,不建议使用make函数为那些重载了operator new和operator delete的类创建对象。

    (四)对象的内存可能无法及时回收

      1. make_shared 只分配一次内存,减少了内存分配的开销。使得控制块和托管对象在同一内存块上分配。而控制块是由shared_ptr和weak_ptr共享的,因此两者共同管理着这个内存块(托管对象+控制块)。

      2. 当强引用计数为0时托管对象被析构(即析构函数被调用),但内存块并未被回收,只有等到最后一个weak_ptr离开作用域时,弱引用也减为0才会释放这块内存块。原本强引用减为0时就可以释放的内存, 现在变为了强引用和弱引用都减为0时才能释放, 意外的延迟了内存释放的时间。这对于内存要求高的场景来说, 是一个需要注意的问题。

      3.因此,当内存紧张且托管对象非常大时,如果weak_ptr的生命期比shared_ptr更长时不建议使用make_shared

    【编程实验】make系列函数的优劣

    #include <iostream>
    #include <memory> //for smart pointer
    #include <vector>
    
    using namespace std;
    
    class Widget
    {
    public:
        Widget(){}
        Widget(int x, int y){ cout << "Widget(int x, int y)" << endl; }
        Widget(const std::initializer_list<int> li) { cout << "Widget(std::initializer_list<int> li)"<< endl; }
    };
    
    void processWidget(std::shared_ptr<Widget> spw, int priority){}
    int computePriority() { /*throw 1;*/ return 0; }//假设该函数会抛出异常
    
    class ReallyBigType {};//大对象
    
    int main()
    {
        //1. make系列函数的优势
        //1.1 避免代码冗余,减少重复书写类型
        auto upw1(std::make_unique<Widget>());    //使用make系列函数,Widget只需写一次
        std::unique_ptr<Widget> upw2(new Widget); //使用new,Widget需写二次。
    
        //1.2 make系统异常安全性更高
        //在将实参传递processWidget前,各个参数时必须先被计算出来,假设顺序如下(因编译器和调用约定而异)
        //A. 先new Widget,即一个Widget对象在堆上创建。
        //B. 执行computePriority,但假设此时该函数产生异常,那上面的堆对象就会泄漏。
        //C. 正常流程下,应执行shared_ptr构造函数,但由于第2步的异常,使得第1步分配的堆对象永远不会被这个
        //   shared_ptr接管(实际上该shared_ptr自己都没有机会创建),于是资源泄漏!
        processWidget(shared_ptr<Widget>(new Widget), computePriority());//潜在资源泄漏!
        
        //异常安全!
        processWidget(make_shared<Widget>(), computePriority()); //如果make_shared首先被调用当computePriority
                                                                 //发生异常时,则之前的shared_ptr会被释放,
                                                                 //从而释放Widget对象。如果computePriority先
                                                                 //调用,则make_shared没有机会被调用,也就不会
                                                                 //有资源泄漏!
        //1.3 make_shared一次性分配内存
        auto spw1 = std::make_shared<Widget>(); //一次性分配一个内存单块,可容纳Widget对象和控制块内存
        std::shared_ptr<Widget> spw2(new Widget); //2次分配:new和分配控制块各一次。
    
        //2. make系列函数的局限性
        //2.1 make不能自定义删除器
        auto widgetDeleter = [](Widget* pw) {delete pw; };
        std::unique_ptr<Widget, decltype(widgetDeleter)> upw3(new Widget, widgetDeleter);
        std::shared_ptr<Widget> spw3(new Widget, widgetDeleter);
    
        //2.2 make系列函数不能接受{}初始化
        auto upv = std::make_unique<std::vector<int>>(10, 20); //10个元素,每个都是20。而不是只有两个元素
        auto spv = std::make_shared<std::vector<int>>(10, 20); //同上
    
        auto pw1 = new Widget(10, 20);   //使用圆括号,匹配Widget(int x, int y)
        auto pw2 = new Widget{ 10, 20 }; //使用大括号,匹配Widget(initializer_list)
        delete pw1;
        delete pw2;
    
        auto spw = std::make_shared<Widget>(10, 20); //使用圆括号,匹配Widget中非initializer_list形参的构造函数
        //auto spw = std::make_shared<Widget>({10,20}); //error,make无法转发大括号初始化列表(原因见《完美转发》一课)
        auto initList = { 10, 20 }; //initList推导为initializer_list<int>
        auto splst = std::make_shared<Widget>(initList); //ok,匹配Widget(const std::initializer_list<int> li) 
    
        //2.3 对象的内存可能无法及时回收
        auto pBigObj = std::make_shared<ReallyBigType>(); //通过make_shared创建大对象
        //...   //创建指向大对象的多个std::shared_ptr和std::weak_ptr,并使用这些智能指针来操作对象
        //...   //最后一个指向大对象的shard_ptr在此析构,但若干weak_ptr仍然存在
        //...   //此时,内存块只析构,还没回收。因为weak_ptr还共享着内存块中的控制块
        //...   //最后一个指向大对象的weak_ptr析构,内存块(托管对象+控制块)才被回收。由于weak_ptr的生命期比shared_ptr长,
                //出现了内存块延迟回收的现象。
    
        //使用new方法则不会出现上述现象
        shared_ptr<ReallyBigType> pBigObj2(new ReallyBigType); //通过new,而不是make_shared创建
        //...   //同前,创建指向多个指向大对象的shared_ptr和weak_ptr。
        //...   //最后一个指向大对象的shard_ptr在此析构,但若干weak_ptr仍然存在。此时大对象的内存由于强引用为0,被回收。
        //...   //此阶段,仅控制块占用的内存处于未回收状态。
        //...   //最后一个指向该对象的weak_ptr析构,控制块被回收。
    
        return 0;
    }
  • 相关阅读:
    JS数据类型
    javaws运行jnlp文件被阻止(安全级别过高)
    linux sysctl的使用
    面向对象-特性property
    面向对象-封装
    面向对象-鸭子类型
    面向对象-多态与多态性
    面向对象-抽象类
    面向对象-组合
    面向对象-在子类使用父类的对象或属性
  • 原文地址:https://www.cnblogs.com/5iedu/p/11625644.html
Copyright © 2020-2023  润新知