一、为什么推荐 make_shared/make_unique,而不是直接用 new?
原因 1:杜绝内存泄漏,保证「绝对的异常安全」
直接使用new+ 智能指针构造的写法,存在不可避免的内存泄漏风险 ,这是 C++ 标准委员会推荐 make 系列的首要原因。
风险场景:裸 new 的内存泄漏根源
cpp
void func(std::shared_ptr<A> a, std::shared_ptr<B> b);
// 危险调用:存在内存泄漏风险!
func(std::shared_ptr<A>(new A), std::shared_ptr<B>(new B));
问题本质 :C++ 编译器对函数参数的求值顺序是未定义的,编译器可能的执行顺序:
- 执行
new A→ 分配堆内存,构造 A 对象; - 执行
new B→ 分配堆内存,构造 B 对象; - 构造第一个
shared_ptr<A>,接管new A的内存; - 构造第二个
shared_ptr<B>,接管new B的内存。
如果步骤 2 执行时抛出异常 (比如 B 的构造函数抛异常、new 分配失败),那么步骤 1 分配的new A的内存,没有任何智能指针接管,这块内存永远无法释放 → 内存泄漏!
无风险方案:make 系列彻底解决该问题
cpp
func(std::make_shared<A>(), std::make_shared<B>());
原因 :new的内存分配操作被封装在 make 系列函数的内部 ,函数内部的执行逻辑是「先分配内存,再构造智能指针」,且函数调用的栈帧保证:只要内存分配完成,必然会被智能指针接管。
哪怕构造过程抛异常,make 函数内部的栈展开机制会保证已分配的内存被释放,从根源上杜绝了这种场景的内存泄漏。
原因 2:减少内存分配次数,降低内存碎片
这是make_shared独有的核心优势,make_unique和new+unique_ptr的性能几乎一致,这一点一定要区分开!
shared_ptr 的内存结构
std::shared_ptr的实现包含两个独立的内存块:
- 托管对象块 :存储
new T创建的实际对象(比如 A、B); - 控制块(Control Block) :存储强引用计数、弱引用计数、删除器、析构函数等元信息。
如果用new+shared_ptr:
cpp
std::shared_ptr<A> pa(new A); // 两次内存分配!
编译器会执行两次独立的堆内存分配 :第一次new A分配对象内存,第二次shared_ptr的构造函数分配控制块内存 → 两次 malloc,内存碎片增多,性能损耗。
make_shared 的优化:一次分配,终生受益
cpp
std::shared_ptr<A> pa = std::make_shared<A>(); // 仅一次内存分配!
make_shared会一次性分配一块「连续的大内存」 ,这块内存同时容纳:托管对象 + shared_ptr 的控制块。
- 内存分配次数从「2 次」→「1 次」,减少 malloc 的系统调用开销;
- 连续内存提升 CPU 缓存命中率,访问效率更高;
- 减少内存碎片,内存利用率提升。
二、什么场景下,必须使用 new 而不能用 make_shared/make_unique?
场景 1:需要为智能指针指定「自定义删除器(deleter)」
make_shared和make_unique均不支持传递自定义删除器!
智能指针的一大优势是支持自定义删除器,比如释放文件句柄、释放 malloc 分配的内存、释放数组等,此时必须用new(或裸资源)+ 智能指针构造:
cpp
// 场景1:shared_ptr 自定义删除器(释放文件句柄)
std::shared_ptr<FILE> fp(fopen("test.txt", "r"), fclose);
// 场景2:unique_ptr 自定义删除器(释放数组,C++11)
std::unique_ptr<int[], std::default_delete<int[]>> arr(new int[10]);
// 场景3:自定义lambda删除器
std::shared_ptr<int> p(new int(10), [](int* x) { delete x; std::cout << "自定义释放"; });
场景 2:类的构造函数是「私有 / 保护」的
make_shared/make_unique是全局的模板函数 ,无法访问类的私有 / 保护构造函数。如果类是「单例模式」或「工厂模式」,构造函数私有化,此时只能在类内部用new创建对象,再返回智能指针:
cpp
class Singleton {
private:
Singleton() = default; // 私有构造
public:
static std::shared_ptr<Singleton> getInstance() {
// 只能用new,make_shared无法访问私有构造
return std::shared_ptr<Singleton>(new Singleton);
}
};
场景 3:需要创建「动态数组」的场景
std::make_shared:从 C++11 到 C++26,永远不支持数组类型 ,如果要创建 shared_ptr 管理的数组,必须用new:
cpp
std::shared_ptr<int> arr(new int[10], std::default_delete<int[]>()); // 必须new
std::make_unique:C++14 才支持,且分两种形式:make_unique<T>()(单个对象)、make_unique<T[]>(n)(数组),C++11 无 make_unique ,创建数组只能用new。
场景 4:需要调用类的「重载 operator new」进行定制化内存分配
如果类 T 重载了operator new/operator delete,自定义了内存池 / 内存分配逻辑,make_shared会使用全局的 operator new 分配内存,而非类的重载版本,此时需要用new T()触发类的重载分配逻辑。
场景 5:需要提前析构对象,释放大内存
make_shared的对象内存和控制块绑定,只有强 + 弱引用都为 0 时才释放内存。如果你的对象占用超大内存 ,且有长期存活的weak_ptr,用new+shared_ptr可以让对象内存在强引用为 0 时立即释放,避免内存占用过高。
三、make_shared 的底层实现原理
核心实现思路
template<typename T, typename... Args> std::shared_ptr<T> make_shared(Args&&... args)
- 一次性内存分配 :调用全局内存分配器(operator new),分配一块连续的内存块 ,大小 =
sizeof(T) + shared_ptr的控制块大小,这是性能优化的核心; - 就地构造对象 :在分配好的内存块的「T 对象区域」,使用定位 new(placement new) 调用 T 的构造函数,把转发后的参数传给构造函数;
- 定位 new 的作用:只调用构造函数,不分配内存,刚好契合这里的需求(内存已经分配好了);
- 构造 shared_ptr 并返回:将分配的连续内存块的「控制块地址」和「T 对象地址」绑定到 shared_ptr,让 shared_ptr 接管整个内存块的生命周期。
析构逻辑
当 shared_ptr 的强引用计数为 0 时,调用 T 的析构函数析构对象;当弱引用计数也为 0时,释放整个连续的内存块(对象 + 控制块),一次 free 完成,完美闭环。
四、make_shared 的参数是如何完美转发到 T 的构造函数上的?
make_shared通过可变参数模板 接收任意构造参数 → 通过万能引用 兼容所有值类别 → 通过std::forward 无损转发参数到 T 的构造函数 → 最终通过定位 new就地构造对象。
这是 GCC/libstdc++ 的简化版源码,去掉了编译器细节和异常处理,保留核心逻辑。
cpp
#include <memory>
#include <utility> // 包含std::forward
#include <new> // 包含定位new
// 标准的make_shared实现模板
template <typename T, typename... Args>
std::shared_ptr<T> make_shared(Args&&... args) {
// 步骤1:一次性分配 「对象T + 控制块」的连续内存
using AllocType = std::allocator<T>;
AllocType alloc;
auto mem = alloc.allocate(sizeof(T) + sizeof(typename std::shared_ptr<T>::_ControlBlock));
// 步骤2:完美转发参数 + 定位new 就地构造T对象
T* obj_ptr = new (mem) T(std::forward<Args>(args)...); // 核心:完美转发!
// 步骤3:构造shared_ptr,绑定控制块和对象指针,接管内存
return std::shared_ptr<T>(obj_ptr, [&](T* p) {
p->~T(); // 析构对象
alloc.deallocate(mem, sizeof(T) + sizeof(typename std::shared_ptr<T>::_ControlBlock)); // 释放内存
});
}
std::forward<Args>(args)...:参数包展开,对每一个参数做完美转发;- 转发后的参数直接传给 T 的构造函数,匹配对应的构造重载(无参、单参、多参、移动构造、拷贝构造等);
- 定位 new 保证在已分配的内存上构造对象,不额外分配内存。
五、make_unique 的实现
make_unique在 C++11 中缺失,C++14 才加入标准,它的实现比make_shared简单很多,没有控制块逻辑 ,核心的完美转发逻辑和make_shared完全一致
cpp
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}