重载new和delete来检测内存泄漏
1. 简述
内存泄漏属于资源泄漏的一种,百度百科将内存泄漏分为四种:常发性内存泄漏、偶发性内存泄漏、一次性内存泄漏和隐式内存泄漏。
常发性指:内存泄漏的代码会被多次执行到。偶发性指:内存泄漏的代码只有在特定的条件下才会执行到。一次性指:内存泄漏的代码只会被执行到一次。隐式指:程序在运行中不断的开辟内存,知道程序结束时才释放内存,本质上虽然没有内存泄漏,但是如果这个程序在连续运行很长时间,会耗尽所有内存,导致系统崩溃。
下面首先介绍内存检测的基本原理,然后给出代码样例,最后说明针对四种内存泄漏进行检测的想法。
2. 基本原理
内存泄漏就是new出来的内存没有通过delete合理的释放掉。new和delete这两个函数就是关键点。可以重载new和delete,每次new中开辟一块内存就用链表把这个内存的信息保存下来,每次用delete删除一块内存就从链表中删除这块内存的记录。
3. 代码样例
1
#include
<
iostream
>
2
using
namespace
std;
3
//
---------------------------------------------------------------
4
//
内存记录
5
//
---------------------------------------------------------------
6
class
MemInfo {
7
private
:
8
void
*
ptr;
9
const
char
*
file;
10
unsigned
int
line;
11
MemInfo
*
link;
12
friend
class
MemStack;
13
};
14
//
---------------------------------------------------------------
15
//
内存记录栈
16
//
---------------------------------------------------------------
17
class
MemStack {
18
private
:
19
MemInfo
*
head;
20
public
:
21
MemStack():head(NULL) { }
22
~
MemStack() {
23
MemInfo
*
tmp;
24
while
(head
!=
NULL) {
25
free(head
->
ptr);
//
释放泄漏的内存
26
tmp
=
head
->
link;
27
free(head);
28
head
=
tmp;
29
}
30
}
31
void
Insert(
void
*
ptr,
const
char
*
file, unsigned
int
line) {
32
MemInfo
*
node
=
(MemInfo
*
)malloc(
sizeof
(MemInfo));
33
node
->
ptr
=
ptr; node
->
file
=
file; node
->
line
=
line;
34
node
->
link
=
head; head
=
node;
35
}
36
void
Delete(
void
*
ptr) {
37
MemInfo
*
node
=
head;
38
MemInfo
*
pre
=
NULL;
39
while
(node
!=
NULL
&&
node
->
ptr
!=
ptr) {
40
pre
=
node;
41
node
=
node
->
link;
42
}
43
if
(node
==
NULL)
44
cout
<<
"
删除一个没有开辟的内存
"
<<
endl;
45
else
{
46
if
(pre
==
NULL)
//
删除的是head
47
head
=
node
->
link;
48
else
49
pre
->
link
=
node
->
link;
50
free(node);
51
}
52
}
53
void
Print() {
54
if
(head
==
NULL) {
55
cout
<<
"
内存都释放掉了
"
<<
endl;
56
return
;
57
}
58
cout
<<
"
有内存泄露出现
"
<<
endl;
59
MemInfo
*
node
=
head;
60
while
(node
!=
NULL) {
61
cout
<<
"
文件名:
"
<<
node
->
file
<<
"
,
"
<<
"
行数:
"
<<
node
->
line
<<
"
,
"
62
<<
"
地址:
"
<<
node
->
ptr
<<
endl;
63
node
=
node
->
link;
64
}
65
}
66
};
67
//
---------------------------------------------------------------
68
//
全局对象 mem_stack记录开辟的内存
69
//
---------------------------------------------------------------
70
MemStack mem_stack;
71
//
---------------------------------------------------------------
72
//
重载new,new[],delete,delete[]
73
//
---------------------------------------------------------------
74
void
*
operator
new
(size_t size,
const
char
*
file, unsigned
int
line) {
75
void
*
ptr
=
malloc(size);
76
mem_stack.Insert(ptr, file, line);
77
return
ptr;
78
}
79
void
*
operator
new
[](size_t size,
const
char
*
file, unsigned
int
line) {
80
return
operator
new
(size, file, line);
//
不能用new
81
}
82
void
operator
delete(
void
*
ptr) {
83
free(ptr);
84
mem_stack.Delete(ptr);
85
}
86
void
operator
delete[](
void
*
ptr) {
87
operator
delete(ptr);
88
}
89
//
---------------------------------------------------------------
90
//
使用宏将带测试代码中的new和delte替换为重载的new和delete
91
//
---------------------------------------------------------------
92
#define
new new(__FILE__,__LINE__)
93
//
---------------------------------------------------------------
94
//
待测试代码
95
//
---------------------------------------------------------------
96
void
bad_code() {
97
int
*
p
=
new
int
;
98
char
*
q
=
new
char
[
5
];
99
delete []q;
100
}
101
102
void
good_code() {
103
int
*
p
=
new
int
;
104
char
*
q
=
new
char
[
5
];
105
delete p;
106
delete []q;
107
}
108
//
---------------------------------------------------------------
109
//
测试过程
110
//
---------------------------------------------------------------
111
int
main() {
112
good_code();
113
bad_code();
114
mem_stack.Print();
115
system(
"
PAUSE
"
);
116
return
0
;
117
}
输出结果为:
可见97行开辟的int,没有delete掉,输出结果也显示为97行。
4. 代码说明
4.1 关于new的参数问题。
对于new int,编译器会解释为new(sizeof(int)),对于new int[5],编译器会解释为new(sizeof(int)*5)。因此使用宏定义预编译后,new int就变为new (__FILE__,__LINE__) int,编译器会解释为new(sizeof(int), __FILE__,__LINE__)。
4.2 关于MemStack
MemStack内部也是一个链表结构,注意内部实现不能使用new和delete,只能使用malloc和free来实现链表,因为待测代码中的重载new和delete中调用了MemStack的insert和delete函数,如果insert和delete函数也调用重载后的new和delete的话,会构成死循环的,所以直接使用free和malloc比较好。
MemStack中的析构函数,会释放掉泄漏掉的内存。
5. 使用思考
对于常发性和一次性的内存泄漏代码,直接放入测试就好了。对于偶发性的内存泄漏代码,只要满足特定条件,那么也就转化为常发性或者一次性的内存泄漏了。
对于隐式内存泄漏,由于程序是在很长一段时间之后导致内存耗尽,我们需要长时间观察,每隔一段时间比较一下内存的使用量,如果在一个较长的时间内,内存使用量持续增加,那么可以考虑是内存泄漏。不过调试起来可能会比较麻烦,还是需要重新审视程序设计的。