智能指针
循环引用
Cf.
#include <memory>
#include <stdio.h>
#define PRINT_FUNC do { printf("%s called.\n", __FUNCTION__); }while(0);
using std::shared_ptr;
using std::weak_ptr;
struct B;
struct A
{
A() { PRINT_FUNC }
~A() { PRINT_FUNC }
shared_ptr<B> B_ptr;
};
struct B
{
B() { PRINT_FUNC }
~B() {PRINT_FUNC}
shared_ptr<A> A_ptr; // change to weak_ptr
};
int main()
{
auto a_ptr = std::make_shared<A>(); // a对象
auto b_ptr = std::make_shared<B>(); // b对象
a_ptr->B_ptr = b_ptr;
b_ptr->A_ptr = a_ptr;
return 0;
}
main函数结束时,b_ptr先销毁1,检查引用计数发现a.B_ptr引用了同一个资源(b,此时引用计数为2),将自身引用解掉(此时引用计数减1),由于引用计数尚未减到0,因此,堆上b对象不会销毁。
接着,a_ptr销毁,检查引用计数,发现b.A_ptr引用了同一个资源(a,此时引用计数为2),将自身引用解掉(此时内部的引用计数为1),由于引用计数尚未减到0,因此,堆上a对象不会销毁。
整体看来,a持有b,b持有a,互相阻止了自己的释放。
如何解决?
将A、B中至少一个shared_ptr改为weak_ptr(弱引用)。
shared_ptr<T> sptr = xxx;
weak_ptr<T> wptr = sptr; // 不会增加sptr内部的引用计数,弱引用
注意,如果仅将B中的shared_ptr改为weak_ptr,b对象必然会在a对象之后销毁。因为b对象销毁时,如果a对象未销毁,由于a.B_ptr是shared_ptr,会持有b对象,导致b对象的引用计数无法减为0,因此无法销毁。
std::shared_ptr
Tip
优先使用std::make_shared构造sthared_ptr. unique_ptr也有同样的规则。
写法更简洁
auto sptr(std::make_shared<YourClass>());
std::shared_ptr<YourClass> sptr2(new YourClass());
提升异常安全
假设有函数:
void foo(std::shared_ptr<A> spa, int priority);
用户调用如下:
foo(std::shared_ptr<A>(new A()), compute_priority());
在foo的函数体执行之前需要做三件事:
- new A 分配A对象
- 构造shared_ptr管理对象
- 执行compute_priority函数
编译器只能保证1在2之前执行,如果执行顺序是1-3-2,且compute_priority抛了未捕获的异常,会导致A对象无法释放。
也就是说,在你new一个对象和将它交给shared_ptr管理中间,可能会发生一些事情,导致后面的逻辑走不到。而使用std::make_shared
会在分配对象之后立刻交给shared_ptr管理。
review
T sp = std::shared_ptr<T>(new T()); // #1
T sp = std::make_shared<T>(); // #2
- make_shared只分配一次内存(分配时考虑T对象的大小和shared_ptr控制块的大小)。而第一种写法实际上是两个阶段,第一个阶段是分配内存,构造T对象。第二个阶段是把构造好的对象指针传给std::shared_ptr的构造函数(此时会触发控制块的内存分配),交由shared_ptr管理。因此,如果有weak_ptr引用了这个对象,会导致shared_ptr的控制块无法释放(因为要给weak_ptr提供信息),如果只申请一次内存,将会附带T对象的内存也暂时无法释放2。而使用new就没这个问题。
- make_shared只支持public构造函数
- make_shared异常安全性更好,考虑如下两个调用,
由于函数参数的求值顺序是不确定的,调用#1包含了三个部分:分配内存,构造shared_ptr,调用bar. 如果求值顺序是先分配内存,再调用bar,此时抛了异常,那之前分配的内存就泄漏了。而调用#2只包含两个部分:调用make_shared (这一步会完成内存分配和构造shared_ptr)和调用bar. 问题的关键在于构造完对象之后有没有立刻把原始指针交给shared_ptr,中间会不会发生其他事情。foo(std::shared_ptr(new int(3)), bar()); // #1 foo(std::make_shared<int>(3), bar()); // #2
更多区别参考cppreference
Footnotes
-
总体遵循后创建先销毁的原则。 ↩