指向对象的原始指针不推荐使用,尤其当暴露给别的线程时。Observerable应当保存的不是原始的Observer *,而是别的什么东西,能分辨Observer 是否存活。类似的,如果Observer 要在析构函数里解注册,那么subject_的类型也不能是原始的Observerable*
有经验的C++程序员也许会想到智能指针。没错,这是正道,但也没那么简单,有些关窍需要注意。这两处直接使用shared_ptr是不行的,会形成循环引用,直接造成资源泄露
空悬指针
由两个指针p1和p2,指向堆上的同一个对象Object(下图左边),p1和p2位于不同的线程。假设线程A通过p1指针将对象销毁了,那么p2就成了空悬指针。这是一种典型的C/C++内存错误
要想安全的消耗对象,最好在别的线程都看不到的情况下,偷偷的做(这正是垃圾回收的原因,所有人都用不到的东西就是垃圾)
一个"解决方法"
一个解决空悬指针的方法是,引入一层间接性,让p1和p2所指的对象永久有效。比如下图中的proxy对象,这个对象,持有一个指向Object的指针(也就是说,p1和p2都是二级指针)
当销毁Object之后,proxy对象继续存在,其值变为0。而p2也没有变成空悬指针,它可以通过查看proxy的内容来判断Object是否还活着。如下图
要线程安全的释放Object也不是那么容易,race condition依旧存在。比如p2看第一眼是proxy不是0,正准确去调用Object的成员函数,期间对象已经被p1给销毁了。
问题在于,何时释放proxy指针呢?
一个更好的解决方法
为了安全的释放proxy,我们可以引入引用计数,再把p1和p2都从指针变成对象sp1和sp2。proxy现在有两个成员,指针和计数器
- 一开始,由两个引用,计数值为2
- sp1析构了,引用计数的值减为1
- sp2也析构了,引用计数降为0,可以安全的销毁proxy和object了
等等,这不就是一个计数型智能指针吗?
一个万能的解决方案
引入另外一层间接性,用对象来管理共享资源(object),也就是handler/body的惯用技法。当然,编写线程安全、高效的handler难度很高,我们用现成的库就行
神器shared_ptr/weak_ptr
shared_ptr是引用计数型智能指针。shared_ptr< T>是一个类模板,它只有一个类型参数,使用起来很方便。引用计数是自动化资源管理的常用手法,当引用计数降为0时,对象就被消耗。weak_ptr也是一个引用计数型智能指针,但是它不增加对象的应用次数,即弱(weak)引用。
注意:
- shared_ptr控制对象的生命周期。shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,该x对象就不会析构。当指向对象x的最后一个shared_ptr析构或者reset的时候,x保证会被销毁
系统的避免各种指针错误
C++里可能存续的内存问题大致有这么几个方面:
- 缓冲区溢出
- 空悬指针/野指针
- 重复释放
- 内存泄露
- 不配对的new[]/delete
- 内存碎片
正确的使用智能指针可以轻易解决前5个问题,解决第6个问题需要其他方法,比如内存池
- 缓冲区溢出:用std::vector< char>/std::string或者自己编写Buffer class来管理缓冲区,自动记住缓冲区的长度,并通过成员函数而不是裸指针来修改缓冲区
- 空悬指针/野指针:用shared_ptr和weak_ptr
- 重复释放:用scoped_ptr,只在对象析构的时候释放一次
- 内存泄露:用scoped_ptr,对象析构的时候自动释放内存
- 不配对的new[]/delete:把new[]替换为std::vertor/scoped_array
在现代C++程序中一般不会出现delete语句,资源都是通过只能指针或者容器来管理的
另外,scoped_ptr/shared_ptr/weak_ptr都是值语义,要么是栈上对象,要么是其他对象的直接数据成员,或者标准库容器里面的元素。几乎不会出现下面这种用法
shared_ptr<Foo> *pFoo = new Shared_ptr<Foo>(new Foo);
以及,如果这几种智能指针是对象x的数据成员,而它的模板参数T是个imcomplete类型,那么x的析构函数不能是默认的或者内联的,必须在.cpp文件里面显式定义