- 书接上回,我们继续讲解智能指针
四、make_unique and make_shared
4.1前备知识
-
万能引用(C++20新特性)
-
条件
-
类型推导:它出现在模板参数中,或是使用
auto
关键字进行类型推导。 -
&&
符号:它的类型是T&&
。template <typename T> void func(T&& param); // T&& 是万能引用
-
-
-
std::forward(C++11新特性)
- 作用
- 转发引用(Forwarding Reference): 在模板中,
T&&
是一种特殊的引用类型,可以根据传入的参数是左值还是右值来改变它的行为。 std::forward
的作用: 用于将转发引用保持其原始的值类别。即,如果传入的是左值,则转发为左值;如果传入的是右值,则转发为右值。这就是“完美转发”。- 若不用forward,都会被当作左值,在指针指针中右值实现的移动构造、拷贝优势全无。
- 转发引用(Forwarding Reference): 在模板中,
- 作用
4.2make函数的优势
-
源码优势
-
写起来简单、方便
auto up1(make_unique<Person>()); auto sp1(make_shared<Person>()); auto up2 = new Person(); auto sp2 = new Person();
-
-
异常安全
-
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对象和构造器创建之间,如果这里出现问题,那么指针就泄漏了
-
-
性能提升
- shared_ptr是把两个步骤合并一起,直接申请一大块内存进行分配。这种优化减少了程序的静态尺寸,因为逻辑上只进行了一次内存分配申请,运行时速度也相应提升了。使用
std::make_shared
更进一步免除了在控制块中部分记账信息(bookkeeping information
)的需要,从而减小了程序的总的内存痕迹。
- shared_ptr是把两个步骤合并一起,直接申请一大块内存进行分配。这种优化减少了程序的静态尺寸,因为逻辑上只进行了一次内存分配申请,运行时速度也相应提升了。使用
4.3make函数不适用的场景
-
无法使用自定义deleter
-
小括号和大括号
如果一个类既重载了带有
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);
-
-
重载new、delete
- shared_ptr申请的空间大小往往比普通指针大,包括了控制块,这个时候对象那块使用是自定义的new、delete,而控制块使用全局的new、delete,这就也可能导致偏差。
-
内存释放的滞后性
-
控制块中还有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总结
-
make函数比起直接调用new操作符,减少了源代码的重复,保证了异常安全,并且std::make_shared和std::allocate_shared还能生成更小更快的代码
-
不能使用make函数的情形:要自定义deleter;希望使用大括号{}来初始化对象
-
对于std::shared_ptr,以下两种情况下也不应该使用std::make_shared:
- 自身重载了new和delete操作符的类
- 系统有内存隐患,分配的对象特别大,并且有指向此对象的std::weak_ptr实例存活时间远远长于对象的最后一个std::shared_ptr
- 笔者水平有限,有问题可以和我交流