1. shared_ptr
基于引用计数,
环状引用:
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A{
~A(){
std::cout << "~A()" << std::endl;
}
shared_ptr<B> m_sp;
};
struct B{
~B(){
std::cout << "~B()" << std::endl;
}
shared_ptr<A> m_sp;
};
// 注意:这里是动态内存分配,并没有出现"A对象"和"B对象",这里为了叙述方便,借用"对象"的名称;
// 或者说这里所称的"对象"为匿名对象,被指针管理;匿名对象的销亡不受编译器控制,需要手动释放.
void test(){
shared_ptr<A> spA = make_shared<A>(); // 定义一个A对象,该对象被引用次数为1
shared_ptr<B> spB = make_shared<B>(); // 定义一个B对象,该对象被引用次数为1
std::cout << spA.use_count() << std::endl; // 1
std::cout << spB.use_count() << std::endl; // 1
spA->m_sp = spB; // spB指向的是B对象,此时A对象的成员m_sp也指向了B对象, 所以此时B对象的被引用次数为2
std::cout << spA.use_count() << std::endl; // 1
std::cout << spB.use_count() << std::endl; // 2
spB->m_sp = spA; // spA指向的是A对象,此时B对象的成员m_sp也指向了A对象, 所以此时A对象的被引用次数为2
std::cout << spA.use_count() << std::endl; // 2
std::cout << spB.use_count() << std::endl; // 2
} // (1) spA销亡,A对象的引用次数减1,但是由于B对象还存在,并且B对象的m_sp还指向A对象,
// 所以此时A对象的被引用次数为1,没有减至0,所以A对象的内存没有被释放;
// (2) 接着spB销亡,B对象的引用次数减1,此时B对象的被引用次数由2减至1;
// (3) 注意,此时A对象依然存在,它的成员m_sp指向B对象,B对象的引用次数依然是1,所以B对象依然存在;
// (4) 又因为B对象的存在,B对象的成员m_sp指向A对象,A对象的被引用次数也是1,所以A对象依然存在;
// 所以spA和spB都销亡了,但是A和B对象依然存在并且相互指引.
// 这就是没有A和B对象的引用次数没有减至0的根源,因为A和B对象各自还有持有一个shared_ptr相互指引),造成了内存泄露.
int main() {
test();
return 0;
}
2. weak_ptr
主要用来配合shared_ptr使用,能够打破shared_ptr的环状引用问题.它更像一个shared_ptr的助手,而不像一个智能指针,因为它没有指向的特性,没有->和*运算符,也不能直接接受指针,只能通过share_ptr构造或者默认构造/复制构造,当访问其"管理"的内存对象时,也必须临时用lock()成员函数构造一个临时shared_ptr对象.注意:上面第1点提到的环状引用,实际上是存在shared_ptr的,只是无法再访问它们罢了.
#include <iostream>
#include <memory>
using namespace std;
int main(){
weak_ptr<int> wp1; // 默认构造,没有管理内存
shared_ptr<int> sp = make_shared<int>(10);
weak_ptr<int> wp2(sp); // 用shared_ptr进行构造,由weak_ptr管理内存
weak_ptr<int> wp3(wp2); // 复制构造
auto sp_temp = wp2.lock(); // lock()临时构造一个shared_ptr对象
std::cout << *sp_temp << std::endl; // 10
return 0;
}
weak_ptr并不管理内存,因为它不参与引用计数,也不接受指针,也就无法释放内存;它也没有get()函数可以直接访问内存;
至于它接受shared_ptr参数构造,当shared_ptr的引用计数降为0时,也就把内存释放了,内存是shared_ptr管理的.这就是为什么它没有->和*运算符,访问内存时,必须通过临时构造shared_ptr对象的原因,因为它直接访问内存是不安全的,内存可能已经被释放了.
再看看lock()构造的临时shared_ptr对象时发生的事情:
#include <iostream>
#include <memory>
using namespace std;
int main(){
shared_ptr<int> sp = make_shared<int>(10);
weak_ptr<int> wp(sp);
{
// 这个判断必不可少,因为weak_ptr.lock()取得的shared_ptr可能是空的(当内存被释放时)
// 而shared_ptr重载了类型转换运算符operator bool,如果为空, 返回false
if(shared_ptr<int> p = wp.lock()){ // 临时对象p被构造,int对象的引用计数加1
std::cout << *p << std::endl;
}
} // 离开作用域,临时对象p析构,int对象的引用次数减1,还原到lock调用之前的状态
// (1)可以看到,lock()构造一个shared_ptr临时对象对外界是没有任何影响的
// (2)其次,既然叫lock(),这个函数是原子性的,是线程安全的
// (3)如果允许使用get()获得其普通指针,那么无法判断这个内存是否被释放了
return 0;
}
我们来猜测一下weak_ptr可能的实现:
#include <iostream>
using namespace std;
class SharedPtr{
public:
private:
int m_refTimes;
};
class WeakPtr{
public:
private:
// 注意,在weak_ptr中一定不能有shared_ptr成员,不然就一直有一个shared_ptr指向内存,
// 这样,只要weak_ptr存在,内存就不能被释放,不符号要求
int m_refTimes; // 其对应的shared_ptr管理的对象的引用次数
// 作为配合,shared_ptr也必须要能够管理weak_ptr,所以shared_ptr应该除了对象引用计数外,
// 还需要记录weak_ptr的引用次数
};
如果仅仅使用weak_ptr是没有意义的:
#include <iostream>
#include <memory>
using namespace std;
struct A{
~A(){
std::cout << "~A()" << std::endl;
}
};
void fun(){
weak_ptr<A> wp(make_shared<A>()); // 在"Hello World"打印之前,A对象就被析构了,
// 因为make_shared<A>仅仅构造了一个临时的shared_ptr,
// 本句执行完之后,shared_ptr对象析构,释放其管理的内存
std::cout << "Hello World" << std::endl;
}
int main() {
fun();
return 0;
}
前面提到:"环状引用的根源在于,两个对象各自持有一个强引用指针来相互指引",
那么,要打破这个环很简单,就是要其中某一个对象不持有强引用指针即可.
这里的"强引用"指的是影响引用计数,换言之,weak_ptr的"弱"引用,最关键在于,它不影响引用计数.
所以,我们可以把上面的代码修改一下,即可消除环状引用:
#include <iostream>
#include <memory>
using namespace std;
struct B;
struct A{
~A(){
std::cout << "~A()" << std::endl;
}
shared_ptr<B> m_sp;
};
struct B{
~B(){
std::cout << "~B()" << std::endl;
}
weak_ptr<A> m_wp;
};
void test(){
shared_ptr<A> spA = make_shared<A>();
shared_ptr<B> spB = make_shared<B>();
std::cout << spA.use_count() << std::endl;
std::cout << spB.use_count() << std::endl;
spA->m_sp = spB;
spB->m_wp = spA; // (spB->m_wp)(spA);
std::cout << spA.use_count() << std::endl;
std::cout << spB.use_count() << std::endl;
}
int main() {
test();
shared_ptr<A> spA = make_shared<A>();
weak_ptr<A> wpA(spA);
return 0;
}
参考资料:
用weak_ptr解决shared_ptr的环形引用问题_唐唐唐唐人的博客-CSDN博客_weak_ptr如何解决环形引用
C++ 三种智能指针的使用场景(unique_ptr vs shared_ptr vs weak_ptr) - 零壹生万物