不同之处在于std::make_shared 执行一次堆分配,而调用std::shared_ptr 构造函数执行两次。
堆分配发生在哪里?
std::shared_ptr 管理两个实体:
- 控制块(存储元数据,例如引用计数、类型擦除删除器等)
- 被管理的对象
std::make_shared 执行单个堆分配,占控制块和数据所需的空间。在另一种情况下,new Obj("foo") 为托管数据调用堆分配,std::shared_ptr 构造函数为控制块执行另一个。
如需更多信息,请查看cppreference@的实施说明。
更新一:异常安全
注意 (2019/08/30):由于函数参数的求值顺序发生了变化,这从 C++17 开始不是问题。具体来说,函数的每个参数都需要在评估其他参数之前完全执行。
由于 OP 似乎想知道事物的异常安全方面,我更新了我的答案。
考虑这个例子,
void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ }
F(std::shared_ptr<Lhs>(new Lhs("foo")),
std::shared_ptr<Rhs>(new Rhs("bar")));
由于 C++ 允许对子表达式进行任意求值顺序,因此一种可能的顺序是:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>
现在,假设我们在第 2 步抛出了一个异常(例如,内存不足异常,Rhs 构造函数抛出了一些异常)。然后我们丢失了在第 1 步分配的内存,因为没有任何机会可以清理它。这里问题的核心是原始指针没有立即传递给std::shared_ptr 构造函数。
解决此问题的一种方法是在单独的行上执行它们,这样就不会发生这种任意排序。
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo"));
auto rhs = std::shared_ptr<Rhs>(new Rhs("bar"));
F(lhs, rhs);
解决这个问题的首选方法当然是改用std::make_shared。
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
更新二:std::make_shared的缺点
引用Casey的cmets:
由于只有一次分配,所以在控制块不再使用之前,指针对象的内存不能被释放。 weak_ptr 可以使控制块无限期地保持活动状态。
为什么weak_ptrs 的实例使控制块保持活动状态?
weak_ptrs 必须有一种方法来确定托管对象是否仍然有效(例如,lock)。他们通过检查拥有托管对象的shared_ptrs 的数量来做到这一点,托管对象存储在控制块中。结果是控制块一直处于活动状态,直到 shared_ptr 计数和 weak_ptr 计数都达到 0。
返回std::make_shared
由于std::make_shared 为控制块和托管对象进行了单个堆分配,因此无法独立地为控制块和托管对象释放内存。我们必须等到我们可以释放控制块和托管对象,这恰好是直到没有shared_ptrs 或weak_ptrs 活着。
假设我们通过new 和shared_ptr 构造函数为控制块和托管对象执行了两次堆分配。然后我们在没有shared_ptrs 存活时释放托管对象的内存(可能更早),并在没有weak_ptrs 存活时释放控制块的内存(可能稍后)。