您的位置:首页 > 编程语言 > C语言/C++

C++中野指针问题分析总结

2014-09-20 23:33 876 查看
其实项目一直被野指针困扰,一直在讨论,居然没有什么下文,下文正式由我接受这个问题。

场景是这样:指针间互相引用,一个被删除的时候,另外一个不知道,它再使用的时候就会异常,因为此时指针已经为空了。

我今天提出的解决方案是:

所以实体都在一个地方管理,其他地方都保存其标识号,每次用的时候根据其标识取就行了,这样看起来最简单,保存的地方我们用hash_map,每次操作O(1)复杂度。

现在觉得还造不成hotpot。

不过刚才在网上,早就有网友遇到类似问题,并做了总结。

知然博友,在“安全编程-c++野指针和内存泄漏”提出的思路跟我的类似:

现在全文引用,备查:


摘要:

  尽管C++ 野指针和内存泄漏一直被诟病,但是在实时性很强的应用场合,c++ 仍然是不二之选。游戏服务器开发仍然使用c++ 作为主语言,但是大多结合动态脚本技术,一方面规避了野指针和内存泄露,一方面获得了开发效率和扩展性的红利。但脚本技术不是本文的讨论重点,事实上关于c++ 与 lua的技术文章我也一直在整理中,将会另文别述。今天主要说说在使用c++过程中,如何避免和解决野指针和内存泄漏问题。


野指针:

  野指针的出现会导致程序崩溃,这是每个人都不愿意看到的。Linux会生成coredump文件,可用gdb分析。Win下可以注册unexception获取调用堆栈,将错误信息写到文件中。先分析一下通常出现野指针的场景:

class monster_t
{
protected:
player_t* m_attack;

public:
void handle_ai()
{
if (m_attack)
{
int x = m_attack->get_x();
}
}
}


  问题就在于,m_attack有值,但是对应的对象已经被销毁了。这是大部分野指针出现原因。分析类之间关系可知,monster_t 和 player_t是0-1的关系,monster_t引用player_t,但是player_t甚至都不知道有一个(或N个)monster 引用了自己。所以当player被销毁时,很难做到把所有引用该player_t的地方全部重置。这种问题其实比较常见,比如player中引用connection,而connection又是被网络层管理生命周期的,也同样容易产生野指针情况。常见的解决方式是:

class monster_t
{
protected:
long m_attack_id;

public:
void handle_ai()
{
player_t* attack = obj_mgr.get(m_attack_id);
if (attack)
{
int x = attack->get_x();
}
}
}


另外一种与之相似的方式:

class monster_t
{
protected:
player_t* m_attack;

public:
void handle_ai()
{
if (obj_mgr.is_exist(m_attack))
{
int x = m_attack->get_x();
}
else
{
m_attack = NULL;
}
}
}


  梳理野指针的产生原因后,我们其实需要的是这样的指针:

  一种指针,引用了另一个对象的地址(不然就不是指针了),当目标对象销毁时,该指针自然指向null,而不需要目标对象主动通知重置。

幸运的是,这种指针已经有了,就是weak_ptr; 在boost库中,sharedptr,scopedptr,weakptr统称为smartptr。可以尽量使用智能指针,避免野指针。本人建议尽量使用shared_ptr结合weak_ptr使用。Scoped_ptr本人使用的较少,只是在创建线程对象的时候使用,正好符合不能复制的语义。使用shared_ptr和weak_ptr的示例代码:

class monster_t
{
protected:
weak_ptr<player_t> m_attack;
shared_ptr<player_t> get_attack()
{
return shared_ptr<player_t>(m_attack);
}
public:
void handle_ai()
{
shared_ptr<player_t> attack = get_attack();
if (attack)
{
int x = attack->get_x();
}
}
}


有人问monster_t为什么不直接使用shared_ptr,如果使用shared_ptr就不符合现实的模型了,monster_t显然不应该控制player_t的生命周期,如果使用了shared_ptr,那么可能导致player_t被延迟析构,甚至会导致内存暴涨。这也是shared_ptr的使用误区,所以本人建议尽量shared_ptr和weak_ptr结合用,否则野指针问题解决了,内存泄漏问题又来了。


内存泄漏:

野指针问题可以通过采用良好的编程范式,尽量规避,但总计c++规避内存泄漏的方法却很为难,简单而言尽量保证对象的分配和释放(分别)是单个入口的,这样大部分问题都可以拦截在code review阶段。那么怎么检测内存泄漏呢?

首先说明本方法区别于valgrind等工具,该工具是调试期进行的检测,本文探究的是运行期的检测,确切说是运行期定时输出所有对象的数量到日志中。

首先定义分配、释放对象的接口:

template<typename T>
T* new_obj()
{
T* p = new T();
singleton_t<obj_counter_t<T> >::instance().inc(1);
return p;
}

template<typename T, typename ARG1>
T* new_obj(ARG1 arg1)
{
T* p = new T(arg1);
singleton_t<obj_counter_t<T> >::instance().inc(1);
return p;
}

template<typename T, typename ARG1, typename ARG2>
T* new_obj(ARG1 arg1, ARG2 arg2)
{
T* p = new T(arg1, arg2);
singleton_t<obj_counter_t<T> >::instance().inc(1);
return p;
}
template<typename T>
T* new_array(int n)
{
T* p = new T
;
singleton_t<obj_counter_t<T> >::instance().inc(n);
return p;
}


为了节省篇幅,这里只列举了三种构造的代码,当分配对象时,对应的类型数量增加1,obj_counter 使用原子操作为每一种类型记录其数量。

class obj_counter_i
{
public:
obj_counter_i():m_ref_count(0){}
virtual ~ obj_counter_i(){}
void inc(int n) { (void)__sync_add_and_fetch(&m_ref_count, n); }
void dec(int n) { __sync_sub_and_fetch(&m_ref_count, n);        }
long val() const{ return m_ref_count;                            }

virtual string get_name() { return ""; }
protected:
volatile long m_ref_count;
};
template<typename T>
class obj_counter_t: public obj_counter_i
{
obj_counter_t()
{
singleton_t<obj_counter_t<T> >::instance().reg(this);
}
virtual string get_name() { return TYPE_NAME(T); }
};


相应的当对象被释放的时候,对应的对象数量减一,示例代码如下:

template<typename T>
void del_obj(T* p)
{
if (p)
{
delete p;
singleton_t<obj_counter_t<T> >::instance().dec(1);
}
}


这样就做到了所有的对象的数量都被记录了,可以定时的将对象数量输出到文件:

class obj_counter_summary_t
{
public:
void reg(obj_counter_i* p)
{
m_all_counter.push_back(p);
}

map<string, long> get_all_obj_num()
{
map<string, long> ret;
for (list<obj_counter_i*>::iterator it = m_all_counter.begin(); it != m_all_counter.end(); ++it)
{
ret.insert(make_pair((*it)->get_name(), (*it)->val()));
}
return ret;
}

void dump(const string& path_)
{
ofstream tmp_fstream;
tmp_fstream.open(path_.c_str());
map<string, long> ret = get_all_obj_num();
map<string, long>::iterator it = ret.begin();

time_t timep   = time(NULL);
struct tm *tmp = localtime(&timep);

char tmp_buff[256];
sprintf(tmp_buff, "%04d%02d%02d-%02d:%02d:%02d",
tmp->tm_year + 1900, tmp->tm_mon + 1, tmp->tm_mday,
tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
char buff[1024] = {0};

snprintf(buff, sizeof(buff), "obj,num,%s\n", tmp_buff);
tmp_fstream << buff;

for (; it != ret.end(); ++it)
{
snprintf(buff, sizeof(buff), "%s,%ld\n", it->first.c_str(), it->second);
tmp_fstream << buff;
}

tmp_fstream.flush();
}
protected:
list<obj_counter_i*>    m_all_counter;
};


输出的文件格式为csv格式,方便进一步做数据分析。可以使用我开发的小工具格式化csv数据。url:http://ffown.sinaapp.com/perf/csv.html

文件内容data:

obj,num,20120606-17:01:41

dumy,1111

foo,222

obj,num,20120606-18:01:41

dumy,11311

foo,2422

obj,num,20120606-19:01:41

dumy,41111

foo,24442



总结:

野指针可以使用shared_ptr和weak_ptr结合使用来尽量规避。
使用shared_ptr要尽量小心,否则可能导致对象无法释放,导致内存泄漏。
可以定时输出当前所有对象的数量,来分析是否有内存泄漏,或者内存泄漏是有哪些对象引起的。
本文介绍了记录所有对象的方法,除了可以分析内存泄漏外,也不失为数据分析的一种方法。需要注明的是,本方法不能替代valgrind工具,二者作用不同。
TYPE_NAME 的实现参考

  https://ffown.googlecode.com/svn/trunk/fflib/include/type_i.h

全部示例代码:https://ffown.googlecode.com/svn/trunk/fflib/include/obj_tool.h
2、vimer博友提出了用二级指针来管理

C/C++代码中,野指针问题历来已久,当然,大家都知道new/delete要成对出现:

123A *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

classA

{

public:

C(){}

virtual~C(){}

};

classB

{

public:

B(){

m_pA=NULL;

}

virtual~B(){}

voidSetA(A*p)

{

m_pA=p;

}

private:

A*m_pA;

};

A*pA=newA();

B*pB=newB();

pB->SetA(pA);

deletepA;

pA=NULL;

//此时B中的m_pA已经无效,但是m_pA仍然不等于NULL,所以用 != NULL来判断不会有任何作用

简单来说,即pA被赋值为NULL,对B中的m_pA没有产生影响,那么怎么才能产生影响呢?

我们有两个做法:

第一种,在A的析构函数里面去B.SetA(NULL),但是这个相当于A去操作了B的数据,这是不合理的。而且当外面的指针非常多的时候,也根本不可能实现。

第二种方法呢?是的,我们可以用二级指针。

考虑如下代码:

12345678910111213141516171819202122232425262728293031323334class 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>

usingnamespacestd;

template<typenameT>

classptr_proxy

{

public:

ptr_proxy(constT*pobj=NULL):m_ppobj(NULL),m_pcount(NULL)

{

if(pobj==NULL)

{

return;

}

init(pobj);

}

ptr_proxy(constptr_proxy&rhs)//
拷贝构造函数

{

m_ppobj=rhs.m_ppobj;//
指向同一块内存

m_pcount=rhs.m_pcount;//
使用同一个计数值

add_count();

}

virtual~ptr_proxy()

{

dec_count();

}

/**

* @brief 如果指向的对象被释放了,一定要调用这个函数让他知道

*/

voidset2null()

{

if(m_ppobj)

{

(*m_ppobj)=NULL;

}

}

/**

* @brief 赋值函数

*

* @param rhs 源对象

*

* @return 自己的引用

*/

ptr_proxy&operator=(constptr_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=(constT*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

{

returnNULL;

}

}

/**

* @brief 获取内部关联的obj的指针

*

* @return

*/

T*operator*()

{

returntrue_ptr();

}

/**

* @brief 获取内部关联的obj的个数

*

* @return 个数

*/

intcount()

{

if(m_pcount!=NULL)

{

return*m_pcount;

}

return0;

}

/**

* @brief 判断智能指针是否为空

*

* @return

*/

boolis_null()

{

if(m_ppobj==NULL||(*m_ppobj)==NULL)

{

returntrue;

}

returnfalse;

}

protected:

voidinit(constT*pobj)

{

m_ppobj=new(T*)();

*m_ppobj=(T*)pobj;

m_pcount=newint();//
初始化计数值为 1

*m_pcount=1;

}

voidadd_count()

{

if(m_pcount==NULL)

{

return;

}

(*m_pcount)++;

}

/**

* @brief 计数减1

*/

voiddec_count()

{

if(m_pcount==NULL||m_ppobj==NULL)

{

return;

}

(*m_pcount)--;//
计数值减 1 ,因为该指针不再指向原来内存块了

if(*m_pcount<=0)//
已经没有别的指针指向原来内存块了

{

//我们不去主动析构对象

//free_sptr(*m_ppobj);//把对象析构

if(m_ppobj!=NULL)

{

deletem_ppobj;

m_ppobj=NULL;

}

if(m_pcount!=NULL)

{

deletem_pcount;

m_pcount=NULL;

}

}

}

protected:

T**m_ppobj;

int*m_pcount;

};

template<typenameT>

classIPtrProxy

{

public:

IPtrProxy(){

m_ptr_proxy=(T*)this;

}

virtual~IPtrProxy(){

m_ptr_proxy.set2null();

}

ptr_proxy<T>&get_ptr_proxy()

{

returnm_ptr_proxy;

}

protected:

ptr_proxy<T>m_ptr_proxy;

};

#

我们来写段测试代码测试一下:

输出为:

12isnullthisisprint
这个类最有效的使用场景是当出现大量互指指针时,那么指向对象的指针有效性判断就尤其重要,而这个类可以完美解决这个问题。可能想的比较深的朋友会问,既然引用计数都已经用上了,那么为什么不直接通过引用计数来析构呢?
其实这几天我也在尝试,C++是否能引入完美的引用计数进行对象管理,而最终卡在一个地方,即:如果,在类的构造函数里面,需要将引用计数对象构造出来,那么引用计数就会出现问题,如:

1

2

3

4

5

6

7

8

9

classA

{

public:

A(){

Countt(this);

}

virtual~A(){}

};

Countc=newA();

这个时候就会出现问题,除非把Count构造的计数对象放到一个对象池中管理,但是又会增加对象查找的成本,所以最终放弃了这个想法。

另外一点就是,C/C++的指针在很多情况下是最方便的,过度的封装很可能会弄巧成拙,所以适度就好。

OK,惯例代码还是放到googlecode上:

http://code.google.com/p/vimercode/source/browse/#svn%2Ftrunk%2Fptr_proxy

目前代码使用中没有发现明显问题,欢迎大家交流~
3.还有egmkang网友提出的记录谁引用了自己这个方法:

实现最为简洁:

/*=============================================================================
#
#     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   赋值函数
*
* @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


#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 "test.h"
#include "ptr_proxy.h"
#include <assert.h>
using namespace std;

class object : public referable<object>
{
public:
int x;
int test(){ return x; }
};

void test()
{
object *a = new object;
object *a1 = new object;
ref_ptr<object> b = a;
a->x = 10;
b->test();
assert(b && b->x == 10);

delete a;
assert(!b);
b->test();
b = a1;
assert(b);
}

int main(){
test();
}


暂时准备按照自己的想法来做。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: