通过引用计数解决野指针的问题(C&C++)
C/C++代码中,野指针问题历来已久,当然,大家都知道new/delete要成对出现:
1 2 3 | A *p = new A(); delete p; p = NULL; |
然而现实中却并不是总是如此简单,考虑如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class A { public: C() {} virtual ~C() {} }; class B { public: B() { m_pA = NULL; } virtual ~B() {} void SetA(A* p) { m_pA = p; } private: A* m_pA; }; A* pA = new A(); B* pB = new B(); pB->SetA(pA); delete pA; pA = NULL; //此时B中的m_pA已经无效,但是m_pA仍然不等于NULL,所以用 != NULL来判断不会有任何作用 |
简单来说,即pA被赋值为NULL,对B中的m_pA没有产生影响,那么怎么才能产生影响呢?
我们有两个做法:
第一种,在A的析构函数里面去B.SetA(NULL),但是这个相当于A去操作了B的数据,这是不合理的。而且当外面的指针非常多的时候,也根本不可能实现。
第二种方法呢?是的,我们可以用二级指针。
考虑如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | class A { public: C() {} virtual ~C() {} }; class B { public: B() { m_ppA = NULL; } virtual ~B() {} void SetA(A** pp) { m_ppA = pp; } private: A** m_ppA; }; A** ppA = new (A*)(); (*ppA) = new A(); B* pB = new B(); pB->SetA(ppA); delete (*ppA); (*ppA) = NULL; //这个时候,B中的m_ppA也会收到影响,即*m_ppA == NULL |
这样确实可以解决野指针的问题,但是同时也引入了另一个问题,那就是ppA本身该什么时候释放呢?答案是:当最后一个引用ppA的类释放掉的时候。
最后一个,对,我们可以使用引用计数!
OK,正式放出我们的代码,其中使用了引用计数来确定当最后一个类释放掉的时候,ppA指针的内存被析构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | /*============================================================================= # # FileName: ptr_proxy.h # Desc: 这个类的作用,就是为了解决互指指针,不知道对方已经析构的问题 # # Author: dantezhu # Email: zny2008@gmail.com # HomePage: http://www.vimer.cn # # Created: 2011-06-13 15:24:12 # Version: 0.0.1 # History: # 0.0.1 | dantezhu | 2011-06-13 15:24:12 | initialization # =============================================================================*/ #ifndef __PTR_PROXY_H__ #define __PTR_PROXY_H__ #include <stdio.h> #include <string.h> #include <stdint.h> #include <iostream> #include <memory> #include <sstream> #include <algorithm> #include <string> #include <vector> #include <set> #include <map> using namespace std; template <typename T> class ptr_proxy { public: ptr_proxy(const T* pobj=NULL) : m_ppobj(NULL), m_pcount(NULL) { if (pobj == NULL) { return; } init(pobj); } ptr_proxy(const ptr_proxy& rhs) // 拷贝构造函数 { m_ppobj = rhs.m_ppobj; // 指向同一块内存 m_pcount = rhs.m_pcount; // 使用同一个计数值 add_count(); } virtual ~ptr_proxy() { dec_count(); } /** * @brief 如果指向的对象被释放了,一定要调用这个函数让他知道 */ void set2null() { if (m_ppobj) { (*m_ppobj) = NULL; } } /** * @brief copy构造函数 * * @param rhs 被拷贝对象 * * @return 自己的引用 */ ptr_proxy& operator=(const ptr_proxy& rhs) { if( m_ppobj == rhs.m_ppobj ) // 首先判断是否本来就指向同一内存块 return *this; // 是则直接返回 dec_count(); m_ppobj = rhs.m_ppobj; // 指向同一块内存 m_pcount = rhs.m_pcount; // 使用同一个计数值 add_count(); return *this; // 是则直接返回 } ptr_proxy& operator=(const T* pobj) { if(m_ppobj && *m_ppobj == pobj) // 首先判断是否本来就指向同一内存块 return *this; // 是则直接返回 dec_count(); init(pobj); return *this; } /** * @brief 获取内部关联的obj的指针 * * @return */ T* true_ptr() { if (m_ppobj) { return *m_ppobj; } else { return NULL; } } /** * @brief 获取内部关联的obj的指针 * * @return */ T* operator*() { return true_ptr(); } /** * @brief 获取内部关联的obj的个数 * * @return 个数 */ int count() { if (m_pcount != NULL) { return *m_pcount; } return 0; } /** * @brief 判断智能指针是否为空 * * @return */ bool is_null() { if (m_ppobj == NULL || (*m_ppobj) == NULL) { return true; } return false; } protected: void init(const T* pobj) { m_ppobj = new (T*)(); *m_ppobj = (T*)pobj; m_pcount = new int(); // 初始化计数值为 1 *m_pcount = 1; } void add_count() { if (m_pcount == NULL) { return; } (*m_pcount) ++; } /** * @brief 计数减1 */ void dec_count() { if (m_pcount == NULL || m_ppobj == NULL) { return; } (*m_pcount)--; // 计数值减 1 ,因为该指针不再指向原来内存块了 if( *m_pcount <= 0 ) // 已经没有别的指针指向原来内存块了 { //我们不去主动析构对象 //free_sptr(*m_ppobj);//把对象析构 if (m_ppobj != NULL) { delete m_ppobj; m_ppobj = NULL; } if (m_pcount != NULL) { delete m_pcount; m_pcount = NULL; } } } protected: T** m_ppobj; int* m_pcount; }; template <typename T> class IPtrProxy { public: IPtrProxy() { m_ptr_proxy = (T*)this; } virtual ~IPtrProxy() { m_ptr_proxy.set2null(); } ptr_proxy<T>& get_ptr_proxy() { return m_ptr_proxy; } protected: ptr_proxy<T> m_ptr_proxy; }; #endif |
我们来写段测试代码测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | #include <stdio.h> #include <string.h> #include <stdint.h> #include <iostream> #include <memory> #include <sstream> #include <algorithm> #include <string> #include <vector> #include <set> #include <map> #include "ptr_proxy.h" using namespace std; class A : public IPtrProxy<A> { public: A() {} virtual ~A() {} }; class B : public IPtrProxy<B> { public: B() {} virtual ~B() {} void print() { printf("this is print\n"); } void SetAPtr(const ptr_proxy<A>& pptr) { m_ptr_a = pptr; } void check() { if (m_ptr_a.is_null()) { printf("is null\n"); } else { printf("is not null\n"); } } ptr_proxy<A> m_ptr_a; }; int main(int argc, char **argv) { A* a = new A(); B* b = new B(); b->SetAPtr(a->get_ptr_proxy()); delete a; b->check(); b->get_ptr_proxy().true_ptr()->print(); delete b; return 0; } |
输出为:
is null this is print
这个类最有效的使用场景是当出现大量互指指针时,那么指向对象的指针有效性判断就尤其重要,而这个类可以完美解决这个问题。
可能想的比较深的朋友会问,既然引用计数都已经用上了,那么为什么不直接通过引用计数来析构呢?
其实这几天我也在尝试,C++是否能引入完美的引用计数进行对象管理,而最终卡在一个地方,即:
如果,在类的构造函数里面,需要将引用计数对象构造出来,那么引用计数就会出现问题,如:
1 2 3 4 5 6 7 8 9 | class A { public: A() { Count t(this); } virtual ~A() {} }; Count c = new A(); |
这个时候就会出现问题,除非把Count构造的计数对象放到一个对象池中管理,但是又会增加对象查找的成本,所以最终放弃了这个想法。
另外一点就是,C/C++的指针在很多情况下是最方便的,过度的封装很可能会弄巧成拙,所以适度就好。
OK,惯例代码还是放到googlecode上:
http://code.google.com/p/vimercode/source/browse/#svn%2Ftrunk%2Fptr_proxy
目前代码使用中没有发现明显问题,欢迎大家交流~
原创文章,版权所有。转载请注明:转载自Vimer的程序世界 [ http://www.vimer.cn ]
本文链接地址: http://www.vimer.cn/?p=2207
sf~
[回复]
还是觉得c++加个垃圾回收机制比较好, c++0x也还没有, 只是加了个类似返回引用的东东.
[回复]
应用场景是什么,
共享化的资源为什么不借助单体模式?
[回复]
何时我们需要智能指针?
有三种典型的情况适合使用智能指针:
* 资源所有权的共享
* 要编写异常安全的代码时
* 避免常见的错误,如资源泄漏
共享所有权是指两个或多个对象需要同时使用第三个对象的情况。这第三个对象应该如何(或者说何时)被释放?为了确保释放的时机是正确的,每个使用这个共享资源的对象必须互相知道对方,才能准确掌握资源的释放时间。从设计或维护的观点来看,这种耦合是不可行的。更好的方法是让这些资源所有者将资源的生存期管理责任委派给一个智能指针。当没有共享者存在时,智能指针就可以安全地释放这个资源了。
[回复]
Dante 回复:
六月 17th, 2011 at 5:10 下午
嗯,这个的主要场景是用在了bayonet的状态机项目中,很多地方都有用到,举个最简单的例子:
在一个容器里面有一堆指针,在某个逻辑中指针被delete掉了,但是我不想立即去在容器中erase,因为这样会增加容器的查找效率。我希望能每隔一段时间去对容器遍历一次进行资源回收。
这种情况下就可以用到文中的指针代理,真实的指针已经为null了,我只要遍历一下容器中的指针代理,把那些为空的对象删除掉就行了。
类似的还有很多,比如互指指针,单体无法解决这个问题,还有观察者模式,被观察者析构时通知观察者,等等。
[回复]
一般做法是new多分配一点空间存放引用计数,不过这样实现的话必须要求每个类自己管理,也不方便。试试boost::shared_ptr
[回复]
为何不考虑boost::shared_ptr?
[回复]
boost::shared_ptr + boost::weak_ptr还可以解决循环引用的问题。
用cpp终究躲不开template的
[回复]
Dante 回复:
六月 26th, 2011 at 10:42 下午
嗯,其实文中的使用场景本身不会出现循环引用的问题,因为释放是主动触发的。
至于为什么不选用boost,目前在腾讯还是很少有项目使用boost的,毕竟boost还没有stl那么成熟。
而且一个功能满足自己要求就好,不一定非要追求那些新的东西。
[回复]
saalihmao 回复:
七月 27th, 2011 at 11:03 上午
其实也不需要整个boost,boost::shared_ptr已经完全包含在tr1里面了。版本稍稍新一点的编译器(例如gcc 4.0.0以上,正常的svr的linux包含的gcc版本都不太可能低于4.0.0吧…)就支持的…
std::tr1::shared_ptr和boost::shared_ptr完全一样,tr1也是标准库的一部分,而且也肯定是未来的cpp标准的一部分。
使用这些著名库的好处是完善…虽然功能确实是够了,但是这些库的代码实际上要安全得多,考虑的情况也要多得多… 例如你的实现里面ptr_proxy构造函数应该区分默认和基于指针创建的情况,后者应该是explict,前者应该throw ()不分配内存保证无异常抛出,例如所有的分配空间的地方都应该有内存捕获保证已分配的空间被释放,例如应该考虑支持有些类的destructor不是public的情况,例如应该考虑支持stl的Allocator的自定义资源分配…等等,c++确实是恶心,所以库很重要啊,一直自己造轮子是不行的…
[回复]
http://www.cnblogs.com/egmkang/archive/2011/09/26/2189548.html
这是我写的文章,也可以用来避免野指针,需要自己delete资源.
[回复]
还有一个万能的方法:自己new的东西就自己负责彻底的delete and set null。记得stdc里有个函数判断内存指针是否有效……忘记了。
[回复]