引用计数是一种很实用的设计,我们可以在很多地方看到它的使用:
- 比如GUI中的自动垃圾回收机制,就是这么搞的,比如我们在一个按键的回调中需要创建一个view并显示,那么我们会new一个pageiew的对象,比如var view_instance = new xx::view ,然后present这个view到窗口上显示. 当关闭这个窗口的时候,我们可以令view_instance = null.这样就不再有人引用这个new出来的xx::view, 然后很快,这块内存就会被垃圾回收机制回收掉
- 比如linux中的设备驱动中对硬件资源的管理。举个例子,hal对用户提供的接口有open, close, read, write等。在驱动中,不同的open操作对应的是同一个硬件资源,只有第一次才会真正的分配资源,后面每个使用者open一次只不过是引用计数+1, 每个使用者close则对应引用计数-1,只有引用计数减到0的时候才真正的释放对应的硬件资源
- C++中或者android中的智能指针,强弱指针等
在我看来当一个库提供的接口中含有reference, 我认为它的语义就是,不需要用户来管理内存的分配和释放,只需要用户在合适的时候reference和dereference即可。C++中更进了一步,搞出来了智能指针的概念,把这块工作进一步的自动化了。那么引用计数法的缺点是什么呢?有两个缺点:一个是增加了开销,需要为每个对象维护一个计数。另一个才是真正致命的问题,无法处理循环引用的问题,也就是下面这个例子
#include <iostream> #include <memory> class A; class B; class A { public: std::shared_ptr<B> pointer; ~A() { std::cout << "A 被销毁" << std::endl; } }; class B { public: std::shared_ptr<A> pointer; ~B() { std::cout << "B 被销毁" << std::endl; } }; int main() { std::shared_ptr<A> a = std::make_shared<A>(); std::shared_ptr<B> b = std::make_shared<B>(); a->pointer = b; b->pointer = a; return 0; }
运行结果是 A, B 都不会被销毁,这是因为 a,b 内部的 pointer 同时又引用了 a,b,这使得 a,b 的引用计数均变为了 2,而离开作用域时,a,b 智能指针被析构,但是只是把内部的引用计数减一,这样就导致了 a,b 对象指向的内存区域引用计数不为零,而外部已经没有办法找到这块区域了,也就造成了内存泄露
为了解决这个问题,C++中引入了弱指针 std::weak_ptr,std::weak_ptr是一种弱引用(相比较而言 std::shared_ptr 就是一种强引用)
#include <iostream> #include <memory> class A; class B; class A { public: std::weak_ptr<B> pointer; ~A() { std::cout << "A 被销毁" << std::endl; } }; class B { public: std::shared_ptr<A> pointer; ~B() { std::cout << "B 被销毁" << std::endl; } }; int main() { std::shared_ptr<A> a(new A); //A的引用计数是1 std::shared_ptr<B> b(new B); //B的引用计数是1 a->pointer = b; //B的引用计数是1 b->pointer = a; //A的引用计数是2 return 0; }
为了更好的理解为什弱引用可以解决这个问题,我们逐行分析一下源码,看看到底干了什么。首先先看下shared ptr的实现:
template <class T> class WeakPtr; //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用 template <class T> class SharePtr { public: SharePtr(T *p = 0) : _ptr(p) //通过普通指针构造一个智能指针 { cnt = new Counter(); //构造的时候创建一个Counter,用于计数 if (p) cnt->s = 1; //初始化为1 cout << "in construct " << cnt->s << endl; } ~SharePtr() { release(); } SharePtr(SharePtr<T> const &s) //拷贝构造 { cout << "in copy con" << endl; _ptr = s._ptr; //指向同一个对象 (s.cnt)->s++; //引用计数+1 cout << "copy construct" << (s.cnt)->s << endl; cnt = s.cnt; //把引用计数同步到当前shared ptr } SharePtr(WeakPtr<T> const &w) //为了用weak_ptr的lock(),来生成share_ptr用,需要拷贝构造用 { cout << "in w copy con " << endl; _ptr = w._ptr; (w.cnt)->s++; cout << "copy w construct" << (w.cnt)->s << endl; cnt = w.cnt; } SharePtr<T> &operator=(SharePtr<T> &s) { if (this != &s) { release(); //解除或者-1当前shared ptr的引用 (s.cnt)->s++; cout << "assign construct " << (s.cnt)->s << endl; cnt = s.cnt; _ptr = s._ptr; //重定向到新的ptr } return *this; } T &operator*() { return *_ptr; } T *operator->() { return _ptr; } friend class WeakPtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值 protected: void release() { cnt->s--; cout << "release " << cnt->s << endl; if (cnt->s < 1) { delete _ptr; //无强引用,释放这块内存;即使有弱引用也要释放 if (cnt->w < 1) //有弱引用,保留cnt指向的这块内存 { delete cnt; cnt = NULL; } } } private: T *_ptr; Counter *cnt; };
然后再看一下弱引用的源码:
template <class T> class WeakPtr { public: //给出默认构造和拷贝构造,其中拷贝构造不能有从原始指针进行构造 WeakPtr() { _ptr = 0; cnt = 0; } WeakPtr(SharePtr<T> &s) : _ptr(s._ptr), cnt(s.cnt)//从shared ptr进行构造 { cout << "w con s" << endl; cnt->w++; //对s的对象的弱引用计数+1 } WeakPtr(WeakPtr<T> &w) : _ptr(w._ptr), cnt(w.cnt) { cnt->w++; } ~WeakPtr() { release(); } WeakPtr<T> &operator=(WeakPtr<T> &w) { if (this != &w) { release(); cnt = w.cnt; cnt->w++; _ptr = w._ptr; } return *this; } WeakPtr<T> &operator=(SharePtr<T> &s) { cout << "w = s" << endl; release(); cnt = s.cnt; cnt->w++; _ptr = s._ptr; return *this; } SharePtr<T> lock() { return SharePtr<T>(*this); } bool expired() { if (cnt) //结合上面的shared ptr看,如果对象被析构,这个有可能是null { if (cnt->s > 0)//若还有强引用,则说明没有expired { cout << "empty" << cnt->s << endl; return false; } } return true; //是null的话,铁定内存已经被释放了 } friend class SharePtr<T>; //方便weak_ptr与share_ptr设置引用计数和赋值 protected: void release() { if (cnt) { cnt->w--; cout << "weakptr release" << cnt->w << endl; if (cnt->w < 1 && cnt->s < 1) { cnt = NULL; //可以看到弱引用计数不会影响对象的释放 } } } private: T *_ptr; Counter *cnt; };
注意,delete一个对象的时候,除了会执行这个对象的析构函数,还会自动执行这个对象内部的其它对象的析构,举个例子:
class B { public: std::weak_ptr<A> pointer; ~B() { std::cout << "B 被销毁" << std::endl; } }; class C { public: ~C() { std::cout << "c的析构" << std::endl; } B mB; }; int main() { C *c = new C; delete c; return 0; }
执行结果为:
那么为什么引入弱引用可以解决循环引用的问题也就迎刃而解了。a被析构的时候,执行release(), 对A的引用计数从2减到1(b还持有一个),然后b析构的时候,执行release, B的引用计数是1减到0(a对B是弱引用),这样就会delete B, B的析构函数会执行,然后B中的对象会被执行析构,也就是一个指向A的shared ptr对象会被析构,于是这个时候A的引用计数由1减到0,A也最终被释放了。
后记:
- 虽然通过弱引用指针可以有效的解除循环引用,但这种方式必须在程序员能预见会出现循环引用的情况下才能使用,也可以是说弱引用仅仅是一种编译期的解决方案,如果程序在运行过程中出现了循环引用,还是会造成内存泄漏的。因此,不要认为只要使用了智能指针便能杜绝内存泄漏
- 弱引用:它仅仅是对象存在时候的引用,当对象不存在时弱引用能够检测到,从而避免非法访问,弱引用也不会修改对象的引用计数。这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存
- 智能指针不是线程安全的,多线程使用时记得加锁