int a = 0;
int *b = malloc (sizeof(int));
b = malloc (sizeof(int));
上面的代码很糟糕,因为它在堆上分配内存然后不释放它,这意味着您无法访问它。但是您还创建了“a”并且从未使用过它,因此您还在堆栈上分配了内存,直到作用域结束才释放。
那么为什么不释放堆上的内存但不释放堆栈上的内存(直到作用域结束)是不好的做法呢?
注意:我知道堆栈上的内存无法释放,我想知道为什么它不被认为是坏的。
作用域结束时,堆栈内存将自动释放。除非您明确释放它,否则在堆上分配的内存将保持占用状态。举个例子:
void foo(void) {
int a = 0;
void *b = malloc(1000);
}
for (int i=0; i<1000; i++) {
foo();
}
运行此代码将使 所需的可用内存减少 1000*1000 字节b
,而当您从调用a
返回时,所需的内存将始终自动释放。foo
很简单:因为你会泄漏内存。内存泄漏很糟糕。泄漏:坏,免费:好。
当调用malloc
orcalloc
或任何 *alloc 函数时,您正在声明一块内存(其大小由传递给分配函数的参数定义)。
与驻留在程序的一部分内存中的堆栈变量不同,可以自由支配,相同的规则不适用于堆内存。您可能出于多种原因需要分配堆内存:堆栈不够大,您需要一个指针数组,但无法知道该数组在编译时需要多大,您需要共享一些内存块(线程噩梦),一个需要在程序中的不同位置(函数)设置成员的结构......
其中一些原因,就其本质而言,意味着一旦指向该内存的指针超出范围,就无法释放该内存。在另一个范围内,另一个指针可能仍然存在,它指向同一个内存块。
但是,正如其中一条评论所提到的,这有一个小缺点:堆内存不仅需要程序员更多地了解,而且它也更昂贵,并且比在堆栈上工作更慢。
所以一些经验法则是:
无论如何,一些例子:
堆栈溢出:
#include <stdio.h>
int main()
{
int foo[2000000000];//stack overflow, array is too large!
return 0;
}
所以,这里我们已经耗尽了堆栈,我们需要在堆上分配内存:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *foo= malloc(2000000000*sizeof(int));//heap is bigger
if (foo == NULL)
{
fprintf(stderr, "But not big enough\n");
}
free(foo);//free claimed memory
return 0;
}
或者,一个数组的示例,其长度取决于用户输入:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *arr = NULL;//null pointer
int arrLen;
scanf("%d", &arrLen);
arr = malloc(arrLen * sizeof(int));
if (arr == NULL)
{
fprintf(stderr, "Not enough heap-mem for %d ints\n", arrLen);
exit ( EXIT_FAILURE);
}
//do stuff
free(arr);
return 0;
}
所以这个列表还在继续......另一个有用的情况malloc
:calloc
一个字符串数组,所有的大小都可能不同。比较:
char str_array[20][100];
在本例str_array
中是一个由 20 个字符数组(或字符串)组成的数组,每个 100 个字符长。但是,如果 100 个字符是您需要的最大值,而平均而言,您只会使用 25 个字符或更少,该怎么办?
您正在用 C 编写,因为它速度很快,而且您的程序不会使用比实际需要更多的资源?那么这不是你真正想要做的。更有可能的是,您想要:
char *str_array[20];
for (int i=0;i<20;++i) str_array[i] = malloc((someInt+i)*sizeof(int));
现在,其中的每个元素都str_array
具有我需要分配的内存量。这样就干净多了。但是,在这种情况下,调用free(str_array)
不会削减它。另一个经验法则是:每个 alloc 调用都必须有一个free
调用来匹配它,所以释放这个内存看起来像这样:
for (i=0;i<20;++i) free(str_array[i]);
注意:
动态分配的内存不是内存泄漏的唯一原因。不得不说。如果你读取一个文件,使用 来打开一个文件指针fopen
,但未能关闭该文件 ( fclose
) 也会导致泄漏:
int main()
{//LEAK!!
FILE *fp = fopen("some_file.txt", "w");
if (fp == NULL) exit(EXIT_FAILURE);
fwritef(fp, "%s\n", "I was written in a buggy program");
return 0;
}
可以编译并运行得很好,但它会包含一个泄漏,只需添加一行即可轻松插入(并且应该插入):
int main()
{//OK
FILE *fp = fopen("some_file.txt", "w");
if (fp == NULL) exit(EXIT_FAILURE);
fwritef(fp, "%s\n", "I was written in a bug-free(?) program");
fclose(fp);
return 0;
}
顺便说一句:如果范围真的很长,那么您可能会尝试将太多内容塞入单个函数中。即便如此,如果你不是:你可以在任何时候释放声明的内存,它不必是当前范围的结尾:
_Bool some_long_f()
{
int *foo = malloc(2000000000*sizeof(int));
if (foo == NULL) exit(EXIT_FAILURE);
//do stuff with foo
free(foo);
//do more stuff
//and some more
//...
//and more
return true;
}
因为在其他答案中多次提到的stack和heap有时是被误解的术语,即使在 C 程序员中也是如此,这是讨论该主题的精彩对话......
那么为什么不释放堆上的内存但不释放堆栈上的内存(直到作用域结束)是不好的做法呢?
堆栈上的内存,例如分配给自动变量的内存,将在退出创建它们的范围时自动释放。是否scope
表示全局文件、函数或函数内的块({...})。
但是堆上的内存,例如使用malloc()
,创建的内存calloc()
,甚至fopen()
分配的内存资源在您明确使用free()
, 或fclose()
为了说明为什么在不释放内存的情况下分配内存是不好的做法,请考虑如果应用程序被设计为自主运行很长时间会发生什么,假设该应用程序用于控制汽车巡航控制的 PID 循环中。并且,在该应用程序中,内存未释放,运行 3 小时后,微处理器中的可用内存耗尽,导致 PID 突然出现异常。“啊!”,你说,“这永远不会发生!” 是的,它确实。(看这里)。(不完全相同的问题,但你明白了)
如果那个单词图片不起作用,那么观察当你在你自己的 PC 上运行这个应用程序(内存泄漏)时会发生什么。(至少查看下图以了解它对我的作用)
您的计算机将表现出越来越缓慢的行为,直到它最终停止工作。您可能需要重新启动才能恢复正常行为。
(我不建议运行它)
#include <ansi_c.h>
char *buf=0;
int main(void)
{
long long i;
char text[]="a;lskdddddddd;js;'";
buf = malloc(1000000);
strcat(buf, "a;lskdddddddd;js;dlkag;lkjsda;gkl;sdfja;klagj;aglkjaf;d");
i=1;
while(strlen(buf) < i*1000000)
{
strcat(buf,text);
if(strlen(buf) > (i*10000) -10)
{
i++;
buf = realloc(buf, 10000000*i);
}
}
return 0;
}
运行此内存猪仅 30 秒后的内存使用情况:
我想这与范围'结束'经常(在函数的末尾)有关,这意味着如果你从创建a
和分配的函数返回b
,你将在某种意义上释放被占用的内存a
,并在剩下的时间里丢失使用的执行内存b
尝试多次调用该函数,您很快就会耗尽所有内存。堆栈变量永远不会发生这种情况(除非递归有缺陷)
当函数离开时(通过重置帧指针),会自动回收局部变量的内存。
问题是你在堆上分配的内存在你的程序结束之前永远不会被释放,除非你明确地释放它。这意味着每次分配更多堆内存时,都会越来越多地减少可用内存,直到最终程序耗尽(理论上)。
堆栈内存是不同的,因为它的布局和使用由编译器确定的可预测模式。它根据给定块的需要扩展,然后在块结束时收缩。
那么为什么不释放堆上的内存但不释放堆栈上的内存(直到作用域结束)是不好的做法呢?
想象一下:
while ( some_condition() )
{
int x;
char *foo = malloc( sizeof *foo * N );
// do something interesting with x and foo
}
x
和都是(“堆栈”)变量 foo
。从逻辑上讲,每个循环迭代中都会创建和销毁每个新实例1;无论此循环运行多少次,程序都只会为每个循环的单个实例分配足够的内存。 auto
但是,每次循环时,都会从堆中分配 N 个字节,并将这些字节的地址写入foo
. 即使变量 foo
在循环结束时不再存在,堆内存仍然被分配,现在你不能free
这样做,因为你失去了对它的引用。所以每次循环运行时,都会分配另外 N 字节的堆内存。随着时间的推移,堆内存会耗尽,这可能会导致代码崩溃,甚至会导致内核崩溃,具体取决于平台。甚至在此之前,您可能会看到代码或在同一台机器上运行的其他进程的性能下降。
对于像 Web 服务器这样长时间运行的进程,这是致命的。你总是想确保你自己清理干净。基于堆栈的变量为您清理,但您负责在完成后清理堆。
x
和foo
在函数入口处分配的堆栈空间。通常,所有局部变量的空间(无论它们在函数内的范围如何)都是一次分配的。