C++智能指针(2)/C++新特性

  • 书接上回,我们继续讲解智能指针

四、make_unique and make_shared

4.1前备知识
  1. 万能引用(C++20新特性)

    • 条件

      1. 类型推导:它出现在模板参数中,或是使用 auto 关键字进行类型推导。

      2. && 符号:它的类型是 T&&

        template <typename T>
        void func(T&& param);  // T&& 是万能引用
        
  2. std::forward(C++11新特性)

    • 作用
      1. 转发引用(Forwarding Reference): 在模板中,T&& 是一种特殊的引用类型,可以根据传入的参数是左值还是右值来改变它的行为。
      2. std::forward 的作用: 用于将转发引用保持其原始的值类别。即,如果传入的是左值,则转发为左值;如果传入的是右值,则转发为右值。这就是“完美转发”。
        • 若不用forward,都会被当作左值,在指针指针中右值实现的移动构造、拷贝优势全无。
4.2make函数的优势
  1. 源码优势

    • 写起来简单、方便

      auto up1(make_unique<Person>());
      auto sp1(make_shared<Person>());
      auto up2 = new Person();
      auto sp2 = new Person();
      
  2. 异常安全

    • shared_ptr是有两个步骤,先new一个对象,再通过shared_ptr的构造器创建共享指针,而make函数是直接申请一大块内存,同时完成

      void processWidget(std::shared_ptr<Widget>spw, int priority);
      processWidget(std::shared_ptr<Widget>(new Widget),computePriority());//潜在的内存泄漏!
      
    • 这里会可能会有问题,因为可能computePriority()的时机在new对象和构造器创建之间,如果这里出现问题,那么指针就泄漏了

  3. 性能提升

    • shared_ptr是把两个步骤合并一起,直接申请一大块内存进行分配。这种优化减少了程序的静态尺寸,因为逻辑上只进行了一次内存分配申请,运行时速度也相应提升了。使用std::make_shared更进一步免除了在控制块中部分记账信息(bookkeeping information)的需要,从而减小了程序的总的内存痕迹。
4.3make函数不适用的场景
  1. 无法使用自定义deleter

  2. 小括号和大括号

    如果一个类既重载了带有std::initializer_list类型参数的构造函数,又重载了不带std::initializer_list类型参数的构造函数,那么在创建实例时,通过大括号{}来创建实例时,会优先调用前一个构造函数,而通过小括号()来创建实例则会优先调用后一个重载版本。make系列函数通过完美转发将参数一股脑扔给了类的构造器,但扔的时候,是用的大括号还是小括号呢?对于一些类来说,这两种方式有本质的区别。

    #include <iostream>
    #include <memory>
    #include <string>
    
    class MyClass {
    public:
        // 构造函数接受 std::initializer_list
        MyClass(std::initializer_list<int> list) {
            std::cout << "Initializer list constructor called\n";
            for (auto elem : list) {
                std::cout << elem << ' ';
            }
            std::cout << '\n';
        }
    
        // 构造函数接受单个 int 参数
        MyClass(int x) {
            std::cout << "Single int constructor called\n";
            std::cout << x << '\n';
        }
    };
    
    int main() {
        // 使用大括号 {} 初始化
        MyClass obj1 = {1}; // 调用 std::initializer_list 构造函数
    
        // 使用小括号 () 初始化
        MyClass obj2(42); // 调用单个 int 构造函数
    
        // 使用 make_unique
        auto obj3 = std::make_unique<MyClass>(1); // 调用单个 int 构造函数
        auto obj4 = std::make_unique<MyClass>(std::initializer_list<int>{1, 2, 3}); // 这也是单个 int 构造函数
    
        return 0;
    }
    
    Initializer list constructor called
    1
    Single int constructor called
    42
    Single int constructor called
    1
    Initializer list constructor called	
    1 2 3
    
    auto up = make_unique<std::vector<int>>(10, 20);
    auto sp = std::make_shared<std::vector<int>>(10,20);
    
    • 到底是10个20,还是一个10、一个20。答案是make_unique只通过小括号接受参数,所以是后者

    • 引用大佬的话是:

      坏消息就是,如果我们希望使用大括号{}来构造std::vector,那还是直接调用new吧。如果希望使用make系列函数来创建使用大括号{}初始化的对象,那么就需要使用到大括号{}的完美转发,但是完美转发不支持转发大括号初始化物。但同时,可以先用auto推导一个用大括号初始化的std::initializer_list的对象,然后再将之扔到make系列函数中去。

      auto initList={10,20};
      auto spv = std::make_shared<std::vector<int>>(initList);
      
  3. 重载new、delete

    • shared_ptr申请的空间大小往往比普通指针大,包括了控制块,这个时候对象那块使用是自定义的new、delete,而控制块使用全局的new、delete,这就也可能导致偏差。
  4. 内存释放的滞后性

    • 控制块中还有weak_ptr的计数,智能指针的控制块和对象都是统一创建释放,只要有weak_ptr还在,那么就不会释放内存,而普通new却不会,只要引用计数为0,就直接释放对象,控制块可以weak_ptr清0再释放

      class ReallyBigType{
      	//...
      };
      auto pBigObj= std::make_shared<ReallyBigType>();//通过make函数创建一个非常大的对象
      
      ...//创建一堆std::shared_ptr和std::weak_ptr干活
          
      ...//最后一个std::shared_ptr被析构,ReallyBigType对象被析构,但是仍有std::weak_ptr实例指向它
          
      ...//这段时间内,原来ReallyBigType对象占据的空间仍然被占用
          
      ...//最后一个std::weak_ptr被析构,最终释放std::make_shared分配的内存
          
      // new
      class ReallyBigType{
      	//...
      };
      auto pBigObj= std::shared_ptr<ReallyBigType>(new ReallyBigType);//通过std::shared_ptr和new创建一个非常大的对象
      
      ...//创建一堆std::shared_ptr和std::weak_ptr干活
          
      ...//最后一个std::shared_ptr被释放,ReallyBigType对象被析构,其所占据的内存也被释放
          
      ...//这段时间内,只有控制块的内存仍被占用
          
      ...//最后一个std::weak_ptr被析构,最终释放控制块的内存
      
4.4总结
  1. make函数比起直接调用new操作符,减少了源代码的重复,保证了异常安全,并且std::make_shared和std::allocate_shared还能生成更小更快的代码

  2. 不能使用make函数的情形:要自定义deleter;希望使用大括号{}来初始化对象

  3. 对于std::shared_ptr,以下两种情况下也不应该使用std::make_shared:

    1. 自身重载了new和delete操作符的类
    2. 系统有内存隐患,分配的对象特别大,并且有指向此对象的std::weak_ptr实例存活时间远远长于对象的最后一个std::shared_ptr
  • 笔者水平有限,有问题可以和我交流
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值