有人可以帮助我理解内存泄漏的概念以及特定数据结构如何促进/阻止它(例如链表,数组等)。不久前我被两个不同的人教过两次 - 由于教学方法的不同,这让我有点困惑。
答案 0 :(得分:7)
维基百科有一个good description on memory leaks。给出的定义是:
A memory leak, in computer science (or leakage, in this context), occurs
when a computer program consumes memory but is unable to release it back
to the operating system.
例如,以下C函数泄漏内存:
void leaky(int n)
{
char* a = malloc(n);
char* b = malloc(n);
// Do something with a
// Do something with b
free(a);
}
当程序员忘记调用n
时,上述函数会泄漏free(b)
个字节的内存。这意味着操作系统的内存减少了n
个字节,以满足对malloc
的进一步调用。如果程序多次调用leaky
,操作系统最终可能会耗尽内存,以便为其他任务分配。
至于问题的第二部分,数据结构没有任何固有的东西可以使它们泄漏内存,但是粗心的数据结构实现可能会泄漏内存。例如,请考虑以下从链表中删除元素的函数:
// I guess you could figure out where memory is leaking and fix it.
void delete_element(ListNode* node, int key)
{
if (node != NULL)
{
if (node->key == key)
{
if (node->prev != NULL) {
// Unlink the node from the list.
node->prev->next = node->next;
}
}
else
{
delete_element(node->next, key);
}
}
}
答案 1 :(得分:3)
我在大多数情况下同意Vijay's的答案,但重要的是要注意当丢失对堆块(指针)的引用时会发生泄漏。两个常见原因是:
1 - 失去指针范围
void foo(void)
{
char *s;
s = strdup("U N I C O R N S ! ! !");
return;
}
在上面,我们已经失去了指针s
的范围,所以我们绝对没有办法释放它。该内存现在在(地址)空间中丢失,直到程序退出并且虚拟内存子系统回收该进程所拥有的所有内容。
但是,如果我们只是将函数更改为return strdup("U N I C O R N S ! ! !");
,我们仍然会引用strdup()分配的块。
2 - 重新指定指针而不保存原始
void foo(void)
{
unsigned int i;
char *s;
for (i=0; i<100; i++)
s = strdup("U N I C O R N S ! ! !");
free(s);
}
在这个例子中,我们丢失了99个对s
曾经指向的块的引用,所以我们实际上只是在最后释放了一个块。同样,这个内存现在丢失,直到程序退出后OS回收它。
另一个典型的误解是,如果程序在退出之前没有释放,则程序退出时仍可访问的内存将被泄露。这在很长一段时间内都不是这样。只有在无法取消引用先前分配的块以释放它时才会发生泄漏。
还应该注意,处理static
存储类型略有不同,如this answer中所述。
答案 2 :(得分:1)
基本上,当程序分配内存并且即使不再需要它也不会释放内存时会发生内存泄漏。
正如您从第二点看到的那样,集合通常往往是内存泄漏的焦点,因为它们包含的内容并不明显,当它们由一个长期存在的对象内部维护时更是如此。
原型内存泄漏是一个缓存(即隐式维护的集合)保存在静态变量中(即最长寿命)。
答案 3 :(得分:0)
Vijay提供的答案向您展示了如何产生内存泄漏。但是,一旦您的程序超出几行代码,发现泄漏可能是一项非常困难的任务。
如果您使用的是Linux,valgrind可以帮助您查找泄漏信息。
在Windows上,您可以使用CRT Debug Heap,它显示泄露的内容,但不显示分配的位置。要显示分配泄漏内存的 where ,您可以使用Memory Validator,这是非常轻松的:要么在Memory Validator的引导下运行程序,要么附加到正在运行的进程。不需要更改来源。他们提供30天的试用期,功能齐全。
答案 4 :(得分:0)
我无法真正添加其他人在定义内存泄漏方面所说的内容,但我可以给你一些关于何时可能发生内存泄漏的注释。
首先想到的是一个执行分配的函数:
int* somefunction(size_t sz)
{
int* mem;
mem = malloc(sz*sizeof(int));
return mem;
}
以这种方式编写函数没有任何遗憾。这与malloc非常相似。问题是,你现在开始做:
int* x = somefunction(5);
很容易忘记,现在这不是一个malloc,来释放x。同样,没有任何关于这一点意味着你将忘记,但我的经验告诉我这是我和其他人忽略的事情。
解决此问题的一个好策略是在函数命名中指出分配发生。所以,调用函数somefunction_alloc
。
第二种情况是线程,特别是fork()
,因为代码都在一个地方。如果您使用函数,多个文件等编码整齐,您几乎总能避免错误,但请记住,在某些范围内所有内容都必须是免费的,包括分配给post()和pre fork的内容。考虑一下:
int main()
{
char* buffer = malloc(100*sizeof(char));
int fork_result = fork();
if ( fork_result < 0 )
{
printf("Error\n");
return 1;
}
elseif ( fork_result == 0 )
{
/* do child stuff */
return 0;
}
else
{
/* do parent stuff */
}
free(buffer);
return 0;
}
这里有一个微妙的错误。父级不会泄漏任何内存,但子级确实,因为它是父级的精确副本,包括堆,但它在释放任何内容之前退出。必须在两个代码路径上都免费。同样,如果fork失败,你仍然没有释放。当你编写这样的代码时很容易错过。更好的方法是创建退出代码变量,如int status = 0;
,并在发生错误的地方修改它,不要在任何结构中使用return ,但允许子代码和父代码路径继续他们应该按计划结束。
也就是说,线程和分叉总是因为它们的性质而使调试变得更加困难。