C内存管理


90

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

有人可以通过代码示例向我展示您何时需要执行“内存管理”的示例?


Answers:


230

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

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

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

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

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

如果在编译时不知道数组有多大,或者如果需要大型数组或结构,则需要“计划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和.NET语言,如C#)使用不同的方法,其中堆会自行清理。第二种方法称为“垃圾收集”,对开发人员来说要容易得多,但是您要付出开销和性能上的损失。这是一个平衡。

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


3
如果要在堆栈上放一些东西,但不知道在编译时有多大,alloca()可以扩大堆栈框架以腾出空间。没有freea(),函数返回时会弹出整个堆栈帧。对于大型分配使用alloca()充满了危险。
DGentry

1
也许您可以添加一两个关于全局变量的内存位置的句子
MichaelKäfer17年

在C中,从不放弃return malloc(),其原因UB,(char *)malloc(size);请参见stackoverflow.com/questions/605845/…–
EsmaeelE

17

这是一个例子。假设您有一个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 GB的内存(可能更多,具体取决于您的内存管理器),并且如果您没有崩溃,则进程可能运行缓慢。

要解决此问题,在使用完malloc()之后,您需要为它调用的所有函数调用free():

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

希望这个例子对您有所帮助!


我更喜欢这个答案。但是我有一个小问题。我希望这样的问题可以通过库来解决,难道没有一个库可以模仿基本数据类型并为它们添加内存释放功能,以便在使用变量时也可以自动释放它们吗?
洛伦佐

没有属于标准​​的一部分。如果您使用C ++,则会得到执行自动内存管理的字符串和容器。
马克哈里森

我知道了,所以有一些第三方库吗?你能给他们起个名字吗?
洛伦佐

9

当您要使用堆而不是堆栈上的内存时,必须执行“内存管理”。如果在运行时之前不知道要创建多大的数组,则必须使用堆。例如,您可能想将某些内容存储在字符串中,但在程序运行之前不知道其内容的大小。在这种情况下,您将编写如下内容:

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

 // Do something with the string...

 free(string); // Free the allocated memory

5

我认为,最简单的回答问题的方式就是考虑指针在C中的作用。指针是一种轻量级但功能强大的机制,它以极大的能力为自己射击,为您提供了极大的自由。

在C语言中,确保您的指针指向您拥有的内存的责任仅属于您自己。除非您放弃了指针,否则这就需要一种有组织且有纪律的方法,这使得编写有效的C语言变得困难。

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

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

我想提及另一种类型的内存分配,该类型已被其他帖子忽略。通过在任何函数外部声明变量,可以静态分配变量。我认为一般来说,这种分配方式很糟糕,因为它被全局变量使用。但是,没有什么可以说使用这种方式分配的内存的唯一方法是在混乱的意大利面条代码中将其作为不规则的全局变量。静态分配方法可以简单地用于避免堆和自动分配方法的某些陷阱。一些C程序员惊讶地发现,大型且复杂的C嵌入式和游戏程序是在完全不使用堆分配的情况下构建的。


4

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

一个例子:

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

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

myString = "abcde";

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


在这里分配5个字节。通过分配一个指针来松开它。任何试图释放该指针的尝试都会导致未定义的行为。注意C字符串不会重载=运算符,没有副本。
马丁·约克

不过,这实际上取决于您使用的malloc。许多malloc运算符对齐8个字节。因此,如果此malloc使用页眉/页脚系统,则malloc将保留5 + 4 * 2(页眉和页脚都为4个字节)。那将是13个字节,而malloc只会给您额外的3个字节用于对齐。我并不是说使用它是一个好主意,因为它只会使malloc如此工作的系统,但是至少要知道为什么做错了是最重要的。
kodai

洛基:我已经编辑了答案,以strcpy()代替=;我认为这是克里斯·BC的意图。
echristopherson

我相信,在现代平台中,硬件内存保护可以防止用户空间进程覆盖其他进程的地址空间。您会遇到细分错误。但这不是C本身的一部分。
echristopherson

4

要记住的一件事是始终将指针初始化为NULL,因为未初始化的指针可能包含伪随机有效内存地址,这会使指针错误静默地进行下去。通过强制使用NULL初始化指针,可以始终捕获是否在使用该指针而无需初始化它。原因是操作系统将虚拟地址0x00000000“连接”到常规保护异常以捕获空指针的使用。


2

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

另一个很好的例子是数据结构的实现,例如链表或二叉树。我没有可在此处粘贴的示例代码,但是您可以轻松地将其搜索出来。


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整除的指针
  • 为自己的某些险恶目的分配额外的空间,
  • 创建存储池秒。

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


学习数据结构是理解内存管理的下一个关键步骤。学习适当地运行这些结构的算法将向您展示克服这些麻烦的适当方法。这就是为什么您会在同一课程中找到讲授的数据结构和算法的原因。
aj.toulan

0

@ 欧元米切利

要补充的一个缺点是,当函数返回时,指向堆栈的指针不再有效,因此您不能从函数返回指向堆栈变量的指针。这是一个常见错误,也是仅使用堆栈变量就无法实现的主要原因。如果您的函数需要返回指针,则必须进行malloc并处理内存管理。


0

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

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

我不喜欢C中的许多隐式转换,因此我倾向于使用强制类型转换使“魔术”更加可见。有时它有助于提高可读性,有时却不能提高可读性,有时它会导致编译器捕获无提示错误。尽管如此,我对此并没有强烈的看法。

如果您的编译器理解C ++样式的注释,则这尤其可能。

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


@echristopherson,谢谢。没错-但是请注意,此问与答是从2008年8月开始的,当时Stack Overflow甚至还没有公开Beta版。那时,我们仍在弄清楚该网站应如何运作。该问题/答案的格式不一定应被视为如何使用SO的模型。谢谢!
2013年

嗯,感谢您指出这一点-那时我还没有意识到网站的各个方面仍在变化中。
echristopherson

0

在C语言中,您实际上有两个不同的选择。一,您可以让系统为您管理内存。或者,您可以自己执行此操作。通常,您希望尽可能长地坚持前者。但是,C中的自动管理内存非常有限,在许多情况下,您将需要手动管理内存,例如:

一个。您希望变量的寿命超过函数,并且您不希望拥有全局变量。例如:

结构对{
   int val;
   结构对* next;
}

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

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

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

C。你想做些很脏的事。例如,我想要一个结构体来表示许多类型的数据,而我不喜欢联合(工会看起来太乱了):

结构数据{ int data_type; long data_in_mem; }; 结构动物{/ * something * /}; 构造人{/ *其他东西* /}; struct animal * read_animal(); struct person * read_person(); / *在主* / 结构数据样本; sampe.data_type = input_type; 开关(input_type){ 情况DATA_PERSON: sample.data_in_mem = read_person(); 打破; 情况DATA_ANIMAL: sample.data_in_mem = read_animal(); 默认: printf(“哦,哦!我警告你,那我会再次对你的操作系统造成错误”); }

瞧,一个长值足以容纳任何东西。请记住要释放它,否则您将后悔。这是我最喜欢的C:D技巧。

但是,通常,您可能希望远离自己喜欢的技巧(T___T)。如果使用频率过高,您迟早会破坏操作系统。只要您不使用* alloc和free,就可以肯定地说您仍然是处女,并且代码看起来仍然不错。


“看,长值足以容纳任何内容”-:/您在说什么,在大多数系统上,长值是4个字节,与int完全相同。它适合指针的唯一原因是long的大小恰好与指针的大小相同。不过,您实际上应该使用void *。
Score_Under

-2

当然。如果创建的对象不在范围内,则可以在其中使用它。这是一个人为的示例(请记住,我的语法将关闭; 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环境中,这对于创建的对象(例如按钮)来说几乎是强制性的,必须存在于任何特定函数(甚至类)的范围之外。


1
嘿,是的,是C ++吗?令人惊讶的是,有人花了五个月的时间给我打电话。
TheSmurf,2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.