C内存管理

时间:2020-03-05 18:42:33  来源:igfitidea点击:

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

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

解决方案

回答

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

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

 // Do something with the string...

 free(string); // Free the allocated memory

回答

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

回答

这是一个例子。假设我们有一个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");
...

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

回答

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

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

回答

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

一个例子:

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

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

myString = "abcde";

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

回答

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

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)使用不同的方法,其中堆会自行清理。第二种方法称为"垃圾收集",对开发人员而言要容易得多,但是我们要付出开销和性能上的损失。这是一个平衡。

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

回答

(我之所以写信,是因为我觉得到目前为止的答案还不是很清楚。)

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

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

获取一个好的调试器...祝我们好运!

回答

@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多得多。感谢我们注意到这一点。

回答

@米切利(Euro Micelli)

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

回答

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

一种。我们希望变量的寿命超过函数,并且我们不希望拥有全局变量。前任:

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。我们想要动态分配内存。最常见的示例是没有固定长度的数组:

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

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):

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,就可以肯定地说我们仍然是处女,并且代码看起来仍然不错。

回答

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

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

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

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

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

回答

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