区别在于
std::make_shared
执行一次堆分配,而调用
std::shared_ptr
构造函数则执行两次。
堆分配发生在哪里?
std::shared_ptr
管理两个实体:
- 控制块(存储元数据,如引用计数、类型擦除删除器等)。
- 被管理的对象。
std::make_shared
执行单次堆分配,占用了控制块和数据所需的空间。在另一种情况下,
new Obj("foo")
为被管理的数据调用堆分配,而
std::shared_ptr
构造函数为控制块执行另一个堆分配。
有关更多信息,请查看
cppreference中的
实现注释。
更新 I:异常安全
注意(2019/08/30):由于函数参数求值顺序的改变,这不再是一个问题,自 C++17 起。具体来说,对函数的每个参数都需要在评估其他参数之前完全执行。
由于提问者似乎想了解异常安全方面的内容,我已更新我的答案。
考虑以下示例:
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"));
更新 II:使用 std::make_shared
的缺点
引用Casey的评论:
因为只有一个分配,所以指针的内存只有在控制块不再使用时才能被释放。一个 weak_ptr
可以使控制块无限期地保持活动状态。
weak_ptr
实例是如何保持控制块存活的?
必须有一种方法让 weak_ptr
确定受管理对象是否仍然有效(例如用于 lock
)。它们通过检查拥有所管理对象的 shared_ptr
数量来完成此操作,该数量存储在控制块中。结果是,控制块存活直至 shared_ptr
计数和 weak_ptr
计数都达到0。
回到 std::make_shared
由于 std::make_shared
为控制块和管理对象同时进行单个堆分配,因此没有办法独立释放控制块和管理对象的内存。我们必须等待直到没有 shared_ptr
或 weak_ptr
存在时才能释放两者的内存。
假设我们改为通过 new
和 shared_ptr
构造函数分别对控制块和管理对象进行两个堆分配。然后,当没有 shared_ptr
存在时,我们释放管理对象的内存(可能更早),并在没有 weak_ptr
存在时释放控制块的内存(可能更晚)。
make_shared
可以编写auto p1(std::make_shared<A>())
,p1 将具有正确的类型。 - Ivan Vergiliev