本文目标
本文默认读者是已经了解了观察者模式智能指针的来龙去脉,智能指针的介绍可以参考我的博客:话说智能指针发展之路。
之前看到循环引用的时候,总是只能看到一个很简单的例子,比如这个c++ 智能指针及 循环引用问题,我觉得挺没意思,因为没有实际的意义,就想找个好点的例子来分享。
所以,这篇文章将专注于展示一个更实际点的例子来说明实际工作中确实会碰到shared_ptr的循环引用的问题,然后再展示如何使用weak_ptr来解决。
编译代码
下文即将展示的代码,编译不需要boost库,只需要编译器支持C++11即可!比如我使用的g++就可以这样来编译:
g++ -std=c++11 example.cpp -o out
循环引用是如何形成的
建议先看上面说到的那个简单的例子,然后再来看下面这个例子。循环引用形成的原因概括起来就是“你中有我,我中有你”。
建议看代码的顺序为:main函数是怎么调用各个接口的,然后再看类的声明,接着再看接口的实现,最后结合输出,分析为什么!
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
// 前向声明,以便Observer能够使用这个类型
class Observable;
// 观察者类
class Observer {
public:
Observer(const char* str);
void update();
void observe(shared_ptr<Observable> so, shared_ptr<Observer> me);
virtual ~Observer();
private:
string m_info;
shared_ptr<Observable> m_observable;
};
// 被观察的物体类
class Observable {
public:
void register_(shared_ptr<Observer> ob);
void notify();
~Observable();
private:
vector<shared_ptr<Observer> > m_observers;
typedef vector<shared_ptr<Observer> >::iterator Iterator;
};
int main() {
// 假设我们必须用到shared_ptr来管理对象
shared_ptr<Observable> p(new Observable());
// 用花括号创建一个局部作用域
{
/*
这三个局部shared_ptr对象,在离开作用域之后就会被销毁。
由于还有一份被Observable对象引用了,还是不会马上析构。
*/
shared_ptr<Observer> o1(new Observer("hello"));
shared_ptr<Observer> o2(new Observer("hi"));
shared_ptr<Observer> o3(new Observer("how are you"));
o1->observe(p, o1);
o2->observe(p, o2);
o3->observe(p, o3);
p->notify();
}
cout << "\nget out now...\n" << endl;
p->notify();
cout << "Observable's use_count is: " << p.use_count() << endl;
return 0;
}
// Observable的接口实现
void Observable::register_(shared_ptr<Observer> ob) {
m_observers.push_back(ob);
}
void Observable::notify() {
Iterator it = m_observers.begin();
while (it != m_observers.end()) {
cout << "notify, use_count = " << it->use_count() << endl;
(*it)->update();
++it;
}
}
Observable::~Observable() {
cout << "~Observable()..." << endl;
}
// Observer的接口实现
Observer::Observer(const char* str)
: m_info(str) {}
void Observer::update() {
cout << "update: " << m_info << endl;
}
void Observer::observe(shared_ptr<Observable> so, shared_ptr<Observer> me) {
so->register_(me);
m_observable = so;
}
Observer::~Observer() {
cout << "~Observer(): " << m_info << endl;
}
运行输出为:
notify, use_count = 2
update: hello
notify, use_count = 2
update: hi
notify, use_count = 2
update: how are youget out now…
notify, use_count = 1
update: hello
notify, use_count = 1
update: hi
notify, use_count = 1
update: how are you
Observable’s use_count is: 4
下面用几个自问自答的问题来分析这个输出:
问题1:为什么第一次notify时的use_count是2?
因为一份引用在main函数里,一份存在Observable对象的vector成员里。
问题2:为什么第二次notify的use_count是1?
因为main函数里的那份引用一旦出了局部的作用域,就析构了,所以shared_ptr的引用计数减少了1,而存在Observable对象里的那一份没有析构,所以计数为1。
问题3:为什么最后Observable对象的use_count是4?
因为main函数里有一个引用(shared_ptr的对象p),而o1,o2,o3这三个对象里分别有一个对同个对象的引用,所以总数是4。
问题4:为什么直到程序结束,仍然没有调用析构函数?
因为,Observable的引用计数是4,即使在离开main函数之前会析构掉其作用域内的shared_ptr<Observable>
对象,计数值也只减少为3,所以它不会析构。
而所有Observer对象的引用计数由于Observable对象没有析构,所以一直保留着一份在其中,引用计数为1,也不会析构。
这样就死锁了嘛,A等B析构,B等A析构。所以谁都没成功析构掉,这就造成了内存泄露!!!至于内存泄露的危害,烦请自行检索资料。
解决方法
weak_ptr和shared_ptr搭配就可以完美解决循环引用的问题!!!
怎么做到的呢?
weak_ptr是一种弱引用,被它引用的资源,计数值不会因为它而改变。
同上,先看示例代码,再来结合输出结果分析:
#include <iostream>
#include <string>
#include <memory>
#include <vector>
using namespace std;
// 前向声明,以便Observer能够使用这个类型
class Observable;
// 观察者类
class Observer {
public:
Observer(const char* str);
void update();
void observe(shared_ptr<Observable> so, shared_ptr<Observer> me);
virtual ~Observer();
private:
string m_info;
shared_ptr<Observable> m_observable;
};
// 被观察的物体类
class Observable {
public:
void register_(weak_ptr<Observer> ob);
void notify();
~Observable();
private:
vector<weak_ptr<Observer> > m_observers;
typedef vector<weak_ptr<Observer> >::iterator Iterator;
};
int main() {
// 假设我们必须用到shared_ptr来管理对象
shared_ptr<Observable> p(new Observable());
// 用花括号创建一个局部作用域
{
/*
这三个局部shared_ptr对象,在离开作用域之后就会被销毁。
由于weak_ptr不参与计数,即使还有一份被Observable对象(弱)引用了,
shared_ptr对象的计数值将降低为0,所以会马上析构。
*/
shared_ptr<Observer> o1(new Observer("hello"));
shared_ptr<Observer> o2(new Observer("hi"));
shared_ptr<Observer> o3(new Observer("how are you"));
o1->observe(p, o1);
o2->observe(p, o2);
o3->observe(p, o3);
p->notify();
cout << "Observable's use_count is: " << p.use_count() << endl;
}
cout << "\nget out now...\n" << endl;
p->notify();
cout << "Observable's use_count is: " << p.use_count() << endl;
return 0;
}
// Observable的接口实现
void Observable::register_(weak_ptr<Observer> ob) {
m_observers.push_back(ob);
}
void Observable::notify() {
Iterator it = m_observers.begin();
while (it != m_observers.end()) {
cout << "notify, use_count = " << it->use_count() << endl;
/*
先将weak_ptr提升为shared_ptr,而shared_ptr重载了bool
操作符,所以直接用if (p)就可以判断所引用的内容是否已经失效。
至于为何要提升为shared_ptr,是因为weak_ptr没有重载->操作符
以及没有提供获取原始指针的接口,所以想要执行update操作,
就得先转换为shared_ptr。
*/
shared_ptr<Observer> p(it->lock());
if (p) {
p->update();
++it;
} else {
cout << "Erase when notify..." << endl;
it = m_observers.erase(it);
}
}
}
// Observer的接口实现
Observer::Observer(const char* str)
: m_info(str) {}
void Observer::update() {
cout << "update: " << m_info << endl;
}
void Observer::observe(shared_ptr<Observable> so, shared_ptr<Observer> me) {
so->register_(me);
m_observable = so;
}
Observer::~Observer() {
cout << "~Observer(): " << m_info << endl;
}
输出结果为:
notify, use_count = 1
update: hello
notify, use_count = 1
update: hi
notify, use_count = 1
update: how are you
Observable’s use_count is: 4
~Observer(): how are you
~Observer(): hi
~Observer(): helloget out now…
notify, use_count = 0
Erase when notify…
notify, use_count = 0
Erase when notify…
notify, use_count = 0
Erase when notify…
Observable’s use_count is: 1
~Observable()…
还是几个自问自答的问题:
问题1:为什么第一次notify时的use_count是1?
因为一份引用在main函数里,而Observable对象里存放的是Observer对象的weak_ptr对象,不参与计数,所以计数值只是1。
问题2:为什么离开作用域时,三个Observer对象就析构了?
因为main函数里的那份引用一旦出了局部的作用域,就析构了,所以shared_ptr的引用计数减少了1,而存在Observable对象里的那一份是弱引用,没有参与计数,所以计数值降低为0(从输出也可以看出来,第二次notify时,use_count变为0了),自然就析构了。
问题3:为什么第二次notify时都显示已经erase了?
因为weak_ptr对象们所引用的资源都已经析构了,所以当它提升为shared_ptr后,程序判断该对象为空,表示资源已经失效了,自然就输出了Erase when notify...
。
问题4:为什么程序结束时Observable对象能成功析构?
因为,在离开局部作用域之前,Observable的引用计数是4;而离开局部作用域时,三个Observer对象都析构了,所以Observable的引用计数减少为1。
然后最后程序结束,即将退出main函数之前,会析构掉其作用域内的Observable对象,所以其计数值减少为0,自然就真正析构了。
综上,至此,很简洁地使用weak_ptr和shared_ptr搭配,避免了互相引用的双方都使用shared_ptr而造成的循环引用问题!
参考
- 本例参考了陈硕在他的《Linux 多线程服务端编程:使用 muduo C++ 网络库》一书中的一个类似的例子,只不过他更侧重于在线程安全的角度来说明,并且代码需要依赖boost库,代码地址为:https://github.com/chenshuo/recipes/blob/master/thread/test/Observer_safe.cc
- cplusplus.com的api说明对学习智能指针很有帮助,因为你能快速全面了解其提供的接口,shared_ptr,weak_ptr