make_shraed & make_unique 替代了new ? 什么场景使用new

一、为什么推荐 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++ 编译器对函数参数的求值顺序是未定义的,编译器可能的执行顺序:

  1. 执行 new A → 分配堆内存,构造 A 对象;
  2. 执行 new B → 分配堆内存,构造 B 对象;
  3. 构造第一个shared_ptr<A>,接管new A的内存;
  4. 构造第二个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_uniquenew+unique_ptr的性能几乎一致,这一点一定要区分开!

shared_ptr 的内存结构

std::shared_ptr的实现包含两个独立的内存块

  1. 托管对象块 :存储new T创建的实际对象(比如 A、B);
  2. 控制块(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_sharedmake_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)

  1. 一次性内存分配 :调用全局内存分配器(operator new),分配一块连续的内存块 ,大小 = sizeof(T) + shared_ptr的控制块大小,这是性能优化的核心;
  2. 就地构造对象 :在分配好的内存块的「T 对象区域」,使用定位 new(placement new) 调用 T 的构造函数,把转发后的参数传给构造函数;
    • 定位 new 的作用:只调用构造函数,不分配内存,刚好契合这里的需求(内存已经分配好了);
  3. 构造 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)...));
}
相关推荐
lly2024062 小时前
PHP 运算符
开发语言
不会c嘎嘎2 小时前
QT中的常用控件(五)
服务器·开发语言·qt
jinmo_C++2 小时前
Leetcode矩阵
算法·leetcode·矩阵
你不是我我2 小时前
【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理
java·开发语言
要加油哦~2 小时前
算法 | 整理数据结构 | 算法题中,JS 容器的选择
前端·javascript·算法
一只小bit2 小时前
Qt 重要控件:多元素控件、容器类控件及布局管理器
前端·c++·qt
期待のcode2 小时前
Java虚拟机堆
java·开发语言·jvm
Larry_Yanan2 小时前
Qt多进程(十)匿名管道Pipe
开发语言·qt
一条咸鱼_SaltyFish2 小时前
Spring Cloud Gateway鉴权空指针惊魂:HandlerMethod为null的深度排查
java·开发语言·人工智能·微服务·云原生·架构