char c;
char"/>

C内存管理

C内存管理

C Memory Management

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

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


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

1
2
3
int  a;
char c;
char d[16];

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

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

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

如果在编译时不知道数组有多大,或者如果需要大型数组或结构,则需要"计划B"。

计划B称为"堆"。通常,您可以创建与操作系统允许的变量一样大的变量,但是您必须自己进行操作。较早的帖子向您显示了一种实现方法,尽管还有其他方法:

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

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

一旦创建了堆变量,问题就在于编译器无法告知您何时使用它,因此您将失去自动释放的能力。这就是您所指的"手动释放"的地方。您的代码现在负责确定何时不再需要该变量,然后释放它,以便将内存用于其他目的。对于上述情况,使用:

1
free(p);

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

在C和C ++中,您有责任清理如上所示的堆变量。但是,有些语言和环境(例如Java和.NET语言,如C#)使用不同的方法,其中堆可以自行清理。第二种方法称为"垃圾收集",对开发人员而言要容易得多,但是您要付出开销和性能上的损失。这是一个平衡。

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


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

1
2
3
4
5
6
7
8
9
char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

您这样称呼它:

1
2
3
4
5
6
7
8
9
10
main()
{
    char *s;
    s = strdup("hello");
    printf("%s
"
, s);
    s = strdup("world");
    printf("%s
"
, s);
}

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

对于这么小的内存,这没什么大不了的,但请考虑以下情况:

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

现在您已经用完了11 GB的内存(可能更多,具体取决于您的内存管理器),并且如果您没有崩溃,则进程可能运行缓慢。

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

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

希望这个例子有帮助!


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

1
2
3
4
5
 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory

我认为回答问题的最简洁方法是考虑指针在C中的作用。指针是一种轻量级但功能强大的机制,它为您提供了极大的自由,却以牺牲自己的能力为代价。

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

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

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

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


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


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

一个例子:

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

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

1
myString ="abcde";

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


(我之所以写,是因为到目前为止我还没有找到答案。)

内存管理值得一提的原因是当您遇到需要创建复杂结构的问题/解决方案时。 (如果您的程序一次崩溃,如果您一次在堆栈上分配了很多空间,那是一个错误。)通常,您需要学习的第一个数据结构是某种列表。这是一个链接的链接,位于我的头顶上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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整除的指针
  • 为自己的某些险恶目的分配额外的空间,
  • 创建内存池

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


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

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


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

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

1
2
3
4
5
6
7
8
9
10
11
struct pair{
   int val;
   struct pair *next;
}

struct pair* new_pair(int val){
   struct pair* np = malloc(sizeof(struct pair));
   np->val = val;
   np->next = NULL;
   return np;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int *my_special_array;
my_special_array = malloc(sizeof(int) * number_of_element);
for(i=0; i

<p>
c. You want to do something REALLY dirty. For example, I would want a struct to represent many kind of data and I don't like union (union looks soooo messy):
</p>


struct data{
  int data_type;
  long data_in_mem;
};

struct animal{/*something*/};
struct person{/*some other thing*/};

struct animal* read_animal();
struct person* read_person();

/*In main*/
struct data sample;
sampe.data_type = input_type;
switch(input_type){
 case DATA_PERSON:
   sample.data_in_mem = read_person();
   break;
 case DATA_ANIMAL:
   sample.data_in_mem = read_animal();
 default:
   printf("Oh hoh! I warn you, that again and I will seg fault your OS");
}

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

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


@米切利(Euro Micelli)

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


@Ted Percival:
...you don't need to cast malloc()'s return value.

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

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

This is especially likely if your compiler understands C++-style comments.

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


当然。如果创建的对象不在范围内,则可以在其中使用它。这是一个人为的示例(请记住,我的语法将关闭; C处于生锈状态,但此示例仍将说明该概念):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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环境中,这对于创建的对象(例如按钮)来说几乎是强制性的,必须存在于任何特定函数(甚至类)范围之外。


推荐阅读