96

我一直听说,在 C 语言中,您必须真正注意如何管理内存。而且我还在开始学习 C,但到目前为止,我根本不需要做任何与内存管理相关的活动。我一直想象着必须释放变量并做各种丑陋的事情。但情况似乎并非如此。

有人可以向我展示(带有代码示例)一个您何时必须进行“内存管理”的示例吗?

4

12 回答 12

241

有两个地方可以将变量放入内存中。当您创建这样的变量时:

int  a;
char c;
char d[16];

变量在“堆栈”中创建。当堆栈变量超出范围时(即代码无法再访问它们时),它们会自动释放。您可能会听到它们被称为“自动”变量,但这已经过时了。

许多初学者示例将仅使用堆栈变量。

堆栈很好,因为它是自动的,但它也有两个缺点:(1)编译器需要提前知道变量有多大,以及(2)堆栈空间有些有限。例如:在 Windows 中,在 Microsoft 链接器的默认设置下,堆栈设置为 1 MB,并且并非所有堆栈都可用于您的变量。

如果你在编译时不知道你的数组有多大,或者如果你需要一个大数组或结构,你需要“plan B”。

B计划被称为“”。您通常可以创建与操作系统允许的一样大的变量,但您必须自己做。较早的帖子向您展示了一种方法,尽管还有其他方法:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(注意堆中的变量不是直接操作的,而是通过指针操作的)

一旦你创建了一个堆变量,问题是编译器不能告诉你什么时候完成它,所以你失去了自动释放。这就是您所指的“手动释放”的用武之地。您的代码现在负责决定何时不再需要该变量,并释放它以便可以将内存用于其他目的。对于上述情况,使用:

free(p);

使第二个选项成为“讨厌的事情”的原因在于,要知道何时不再需要该变量并不总是那么容易。在不需要时忘记释放变量会导致程序消耗更多所需的内存。这种情况称为“泄漏”。在您的程序结束并且操作系统恢复其所有资源之前,“泄漏”的内存不能用于任何事情。如果在实际完成之前错误地释放了堆变量,甚至可能出现更严重的问题。

在 C 和 C++ 中,您负责清理堆变量,如上所示。但是,有些语言和环境(如 Java 和 C# 等 .NET 语言)使用不同的方法,堆会自行清理。第二种方法称为“垃圾收集”,对开发人员来说要容易得多,但会在开销和性能方面付出代价。这是一个平衡。

(我已经掩盖了许多细节,以给出一个更简单但希望更公平的答案)

于 2008-08-24T08:21:22.927 回答
19

这是一个例子。假设您有一个复制字符串的 strdup() 函数:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

你这样称呼它:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

您可以看到程序可以运行,但是您已经分配了内存(通过 malloc)而没有释放它。当您第二次调用 strdup 时,您丢失了指向第一个内存块的指针。

对于这么小的内存来说,这没什么大不了的,但考虑一下这种情况:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

您现在已经使用了 11 gig 的内存(可能更多,取决于您的内存管理器),如果您没有崩溃,您的进程可能运行得很慢。

要解决此问题,您需要在使用完 malloc() 后为使用 malloc() 获得的所有内容调用 free():

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

希望这个例子有帮助!

于 2008-08-24T07:38:05.137 回答
9

当您想在堆上而不是堆栈上使用内存时,您必须进行“内存管理”。如果您直到运行时才知道创建一个数组有多大,那么您必须使用堆。例如,您可能想在字符串中存储一些内容,但在程序运行之前不知道其内容有多大。在这种情况下,你会写这样的东西:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
于 2008-08-24T06:57:26.090 回答
6

我认为回答这个问题的最简洁的方法是考虑指针在 C 中的作用。指针是一种轻量级但功能强大的机制,它为您提供了巨大的自由,但代价是巨大的能力让自己在脚下开枪。

在 C 中,确保您的指针指向您拥有的内存的责任是您自己的责任。这需要一种有组织和有纪律的方法,除非你放弃指针,这使得编写有效的 C 变得困难。

迄今为止发布的答案集中在自动(堆栈)和堆变量分配上。使用堆栈分配确实可以实现自动管理和方便的内存,但在某些情况下(大缓冲区、递归算法)可能会导致堆栈溢出的可怕问题。确切地知道可以在堆栈上分配多少内存很大程度上取决于系统。在某些嵌入式场景中,几十字节可能是您的限制,在某些桌面场景中,您可以安全地使用兆字节。

堆分配不是语言固有的。它基本上是一组库调用,授予您对给定大小的内存块的所有权,直到您准备好返回(“免费”)它。这听起来很简单,但与数不清的程序员悲痛有关。问题很简单(两次释放相同的内存,或者根本不释放[内存泄漏],没有分配足够的内存[缓冲区溢出]等)但难以避免和调试。严格遵守纪律的方法在实践中绝对是强制性的,但当然语言实际上并没有强制要求。

我想提一下其他帖子忽略的另一种类型的内存分配。可以通过在任何函数之外声明变量来静态分配变量。我认为一般来说这种类型的分配会受到不好的评价,因为它被全局变量使用。然而,没有什么能说明使用这种方式分配的内存的唯一方法是在一堆意大利面条代码中作为一个无纪律的全局变量。静态分配方法可以简单地用于避免堆和自动分配方法的一些缺陷。一些 C 程序员惊讶地发现,在构建大型和复杂的 C 嵌入式和游戏程序时根本没有使用堆分配。

于 2008-09-02T03:39:29.537 回答
4

关于如何分配和释放内存,这里有一些很好的答案,在我看来,使用 C 更具挑战性的一面是确保您使用的唯一内存是您分配的内存 - 如果这没有正确完成,您将结束最多的是这个站点的表亲 - 缓冲区溢出 - 您可能正在覆盖另一个应用程序正在使用的内存,结果非常不可预测。

一个例子:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

此时,您已经为 myString 分配了 5 个字节并用“abcd\0”填充它(字符串以 null - \0 结尾)。如果您的字符串分配是

myString = "abcde";

您将在已分配给程序的 5 个字节中分配“abcde”,并且尾随空字符将放在此末尾 - 尚未分配给您使用的内存的一部分,可能是免费,但同样可能被另一个应用程序使用 - 这是内存管理的关键部分,其中一个错误将产生不可预测的(有时是不可重复的)后果。

于 2008-08-24T07:58:13.387 回答
4

A thing to remember is to always initialize your pointers to NULL, since an uninitialized pointer may contain a pseudorandom valid memory address which can make pointer errors go ahead silently. By enforcing a pointer to be initialized with NULL, you can always catch if you are using this pointer without initializing it. The reason is that operating systems "wire" the virtual address 0x00000000 to general protection exceptions to trap null pointer usage.

于 2009-03-29T23:00:00.727 回答
2

此外,当您需要定义一个巨大的数组时,您可能希望使用动态内存分配,例如 int[10000]。你不能只是把它放在堆栈中,因为那样,嗯......你会得到一个堆栈溢出。

另一个很好的例子是数据结构的实现,比如链表或二叉树。我没有要粘贴的示例代码,但您可以轻松地用谷歌搜索它。

于 2008-08-24T07:50:49.473 回答
2

(我写信是因为我觉得到目前为止的答案并不完全正确。)

值得一提的内存管理的原因是当您遇到需要创建复杂结构的问题/解决方案时。(如果您一次在堆栈上分配大量空间,如果您的程序崩溃,那就是一个错误。)通常,您需要学习的第一个数据结构是某种list。这是一个单独的链接,在我的脑海中:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

当然,您还想要一些其他功能,但基本上,这就是您需要内存管理的功能。我应该指出,“手动”内存管理可能有一些技巧,例如,

  • 利用malloc保证(由语言标准)返回可被 4 整除的指针这一事实,
  • 为你自己的一些险恶目的分配额外的空间,
  • 创建内存池s..

获得一个好的调试器......祝你好运!

于 2008-08-24T14:13:13.063 回答
0

@ Ted Percival
......你不需要强制转换 malloc() 的返回值。

你是对的,当然。我相信这一直是正确的,尽管我没有K&R的副本要检查。

我不喜欢 C 中的很多隐式转换,所以我倾向于使用强制转换来使“魔法”更加明显。有时它有助于提高可读性,有时它没有,有时它会导致编译器捕获一个静默错误。尽管如此,我对此并没有强烈的看法,不管怎样。

如果您的编译器理解 C++ 风格的注释,这种情况尤其可能发生。

是的……你在那里抓住了我。我花在 C++ 上的时间比 C 多得多。感谢您注意到这一点。

于 2008-08-25T16:09:04.477 回答
0

@欧洲米切利

要添加的一个负面因素是,当函数返回时,指向堆栈的指针不再有效,因此您不能从函数返回指向堆栈变量的指针。这是一个常见错误,也是您无法仅使用堆栈变量的主要原因。如果您的函数需要返回一个指针,那么您必须 malloc 并处理内存管理。

于 2008-08-25T16:50:17.780 回答
0

在 C 中,您实际上有两种不同的选择。一、可以让系统为你管理内存。或者,你可以自己做。通常,您会希望尽可能长时间地坚持前者。但是,C 中的自动管理内存非常有限,在许多情况下您需要手动管理内存,例如:

一种。您希望变量比函数寿命更长,并且您不想拥有全局变量。前任:

结构对{
   整数值;
   结构对 *next;
}

结构对* new_pair(int val){
   结构对* np = malloc(sizeof(结构对));
   np->val = val;
   np->下一个 = NULL;
   返回 np;
}

湾。你想要动态分配内存。最常见的例子是没有固定长度的数组:

int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(i=0; 我

C。你想做一些非常肮脏的事情。例如,我想要一个结构来表示多种数据,但我不喜欢联合(联合看起来很乱):

结构数据{ 整数数据类型; 长数据_in_mem; }; 结构动物{/*某事*/}; struct person{/*其他东西*/}; 结构动物* read_animal(); 结构人* read_person(); /*在主目录*/ 结构数据样本; 样品.data_type = input_type; 开关(输入类型){ 案例 DATA_PERSON: sample.data_in_mem = read_person(); 休息; 案例数据_动物: sample.data_in_mem = read_animal(); 默认: printf("噢噢噢!我警告你,我会再次对你的操作系统进行分段故障"); }

看,长值足以容纳任何东西。只要记住释放它,否则你会后悔的。这是我最喜欢在 C :D 中获得乐趣的技巧之一。

然而,一般来说,你会想要远离你最喜欢的技巧(T___T)。如果你经常使用它们,你迟早会破坏你的操作系统。只要你不使用 *alloc 和 free,可以肯定地说你还是处女,而且代码看起来还不错。

于 2008-08-27T12:08:54.443 回答
-3

当然。如果您创建的对象存在于您使用它的范围之外。这是一个人为的示例(请记住我的语法将被关闭;我的 C 是生锈的,但这个示例仍将说明这个概念):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

在此示例中,我在 MyClass 的生命周期中使用 SomeOtherClass 类型的对象。SomeOtherClass 对象在多个函数中使用,因此我动态分配了内存: SomeOtherClass 对象在 MyClass 创建时创建,在对象的生命周期内使用了几次,然后在 MyClass 被释放后释放。

显然,如果这是真实的代码,就没有理由(除了可能的堆栈内存消耗)以这种方式创建 myObject,但是当您有很多对象并想要精细控制时,这种类型的对象创建/销毁变得有用当它们被创建和销毁时(例如,您的应用程序在其整个生命周期内不会占用 1GB 的 RAM),并且在 Windowed 环境中,这几乎是强制性的,因为您创建的对象(例如按钮) ,需要存在于任何特定函数(甚至类)范围之外。

于 2008-08-24T07:00:35.320 回答