C内存pipe理

我一直听说在C语言中,你必须真正看你如何pipe理内存。 而且我还在开始学习C语言,但到目前为止,我根本没有必要做任何memory management相关的活动。我总是想象不得不释放variables,做各种丑陋的事情。 但是这似乎并不是这样。

有人能告诉我(代码示例)什么时候你需要做一些“内存pipe理”的例子吗?

有两个地方可以把variables放在内存中。 当你像这样创build一个variables时:

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

variables是在“ 堆栈 ”中创build的。 堆栈variables在超出范围时(即代码无法再达到它们时)会被自动释放。 你可能会听到他们称为“自动”variables,但这已经过时了。

许多初学者的例子将只使用堆栈variables。

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

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

计划B被称为“ ”。 您通常可以创build与操作系统一样大的variables,但您必须自己动手。 之前的post向你展示了一种你可以做到的方法,虽然还有其他的方法:

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

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

一旦你创build一个堆variables,问题是编译器不能告诉你什么时候完成了,所以你失去了自动释放。 这就是你所说的“手动释放”的地方。你的代码现在负责决定什么时候不再需要variables,然后释放它,这样内存可以被用于其他目的。 对于上面的情况,用:

 free(p); 

是什么让第二个选项“讨厌的业务”是,知道什么时候不再需要variables并不总是容易的。 当你不需要它时忘记释放一个variables会导致你的程序消耗更多的内存。 这种情况被称为“泄漏”。 直到程序结束并且操作系统恢复其所有资源时,“泄漏”的内存才能用于任何事情。 如果实际完成之前,您错误地释放了一个堆variables,那么即使是更糟糕的问题也是可能的。

在C和C ++中,你有责任清理你的堆variables,如上所示。 但是,像C#这样的Java和.NET语言等语言和环境使用了不同的方法,在这种情况下堆是自行清理的。 第二种称为“垃圾收集”的方法对开发人员来说更容易,但是您会在开销和性能方面付出代价。 这是一个平衡。

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

这是一个例子。 假设你有一个strdup()函数重复一个string:

 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时,你已经失去了指向第一个内存块的指针。

这对于这个less量的内存来说没有什么大不了的,但是考虑一下情况:

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

你现在用了11个内存(可能更多,取决于你的内存pipe理器),如果你没有崩溃你的进程可能运行得很慢。

为了解决这个问题,你需要在使用malloc()之后调用free()来获得所有的东西:

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

希望这个例子有帮助!

当你想在堆上使用内存而不是在堆栈上时,你必须执行“内存pipe理”。 如果你不知道如何在运行时创build一个数组,那么你必须使用这个堆。 例如,您可能希望将某些内容存储在一个string中,但不知道程序运行之前其内容会有多大。 在这种情况下,你会写这样的东西:

  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.

迄今为止发布的答案集中在自动(堆栈)和堆variables分配上。 使用堆栈分配确实可以实现自动pipe理和方便的内存,但是在某些情况下(大缓冲区,recursionalgorithm)会导致堆栈溢出这个可怕的问题。 确切地知道你可以在堆栈上分配多less内存非常依赖于系统。 在某些embedded式scheme中,几十个字节可能是您的限制,在某些桌面scheme中,您可以安全地使用兆字节。

堆分配不是语言固有的。 它基本上是一组库函数,它会授予你给定大小的内存块的所有权,直到你准备返回('free')它为止。 这听起来很简单,但却与无数的程序员悲痛有关。 问题很简单(释放相同的内存两次,或根本不[内存泄漏],没有分配足够的内存[缓冲区溢出]等),但难以避免和debugging。 高度规范的方法绝对是强制性的,但语言当然并没有强制执行。

我想提到其他types的内存分配已被其他职位忽略。 可以通过在variables之外声明variables来静态分配variables。 我认为这种types的分配通常会被全局variables所使用,这是一个糟糕的说唱。 然而没有什么说使用这种方式分配内存的唯一方法就是在混乱的意大利面代码中作为一个没有规律的全局variables。 静态分配方法可以简单地用来避免堆中的一些陷阱和自动分配方法。 一些C程序员惊讶地发现,大型的,复杂的Cembedded式和游戏程序都是在没有使用堆分配的情况下构build的。

需要记住的是始终将指针初始化为NULL,因为未初始化的指针可能包含一个伪随机有效内存地址,可以使指针错误静默地前进。 通过执行一个用NULL初始化的指针,如果你正在使用这个指针而不是初始化它,你总是可以捕获它。 原因是操作系统将虚线地址0x00000000“连线”到一般的保护例外以捕获空指针的使用。

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

一个例子:

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

在这一点上,你已经为myString分配了5个字节,并填充了“abcd \ 0”(string结尾为空 – \ 0)。 如果你的string分配是

 myString = "abcde"; 

你将在你已经分配给你的程序的5个字节中分配“abcde”,尾部的空字符将被放在这里 – 这部分内存还没有被分配给你使用,可能是免费,但同样可以被另一个应用程序使用 – 这是内存pipe理的关键部分,其中一个错误会导致不可预知的(有时是不可重复的)后果。

当你需要定义一个巨大的数组时,你也可以使用dynamic内存分配,比如int [10000]。 你不能只把它放在堆栈中,因为那么呃…你会得到一个堆栈溢出。

另一个很好的例子是数据结构的实现,比如链表或者二叉树。 我没有示例代码粘贴在这里,但你可以很容易地谷歌。

(我正在写,因为我觉得迄今为止的答案不太符合标准。)

值得一提的内存pipe理的原因是当你有一个问题/解决scheme,需要你创build复杂的结构。 (如果你的程序崩溃,如果你一次分配到堆栈上的空间,这是一个错误。)通常,你需要学习的第一个数据结构是某种列表 。 这是一个单一的链接,从我的头顶上:

 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; } 

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

  • 使用malloc保证(通过语言标准)返回一个可以被4整除的指针的事实,
  • 为自己的一些阴险目的分配额外的空间,
  • 创build内存池

得到一个好的debugging器…祝你好运!

@ Euro Micelli

一个负面的添加是指向堆栈的指针在函数返回时不再有效,所以你不能从函数返回一个指向堆栈variables的指针。 这是一个常见的错误,也是你为什么只用堆栈variables无法解决的主要原因。 如果你的函数需要返回一个指针,那么你必须malloc并处理内存pipe理。

@ Ted Percival :
…你不需要inputmalloc()的返回值。

当然,你是对的。 我相信这一直是真的,虽然我没有K&R的副本来检查。

我不喜欢C中的很多隐式转换,所以我倾向于使用强制转换来使“魔术”更加可见。 有时它有助于可读性,有时它不会,并且有时会导致编译器捕获一个无声的错误。 不过,对于这个问题,我还没有一个强烈的意见。

如果你的编译器理解了C ++风格的注释,那么这是特别有可能的。

是的,你在那里发现了我。 我比C花费了更多的时间在C ++上。感谢您的注意。

在C中,你实际上有两个不同的select。 一,你可以让系统为你pipe理内存。 或者,你可以自己做。 一般来说,你会尽可能地坚持前者。 但是,C中自动pipe理的内存非常有限,在很多情况下您需要手动pipe理内存,例如:

一个。 你想variables超出function,你不想有全局variables。 例如:

 struct pair {
    int val;
    struct pair * next;
 }

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

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

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

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

结构数据{ int data_type; long data_in_mem; }; struct animal {/ * something * /}; struct person {/ *其他的东西* /}; 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,可以肯定地说你还是处女,而且代码看起来还是很不错的。

当然。 如果你创build了一个在你使用的范围之外的对象,下面是一个人为的例子(记住我的语法将会closures,我的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的生命周期中使用了SomeOtherClasstypes的对象。 SomeOtherClass对象用于几个函数,所以我dynamic地分配内存:创buildMyClass时创buildSomeOtherClass对象,在对象的整个生命周期中使用多次,然后释放MyClass后释放。

显然,如果这是真正的代码,那么除了可能的堆栈内存消耗之外,没有任何理由以这种方式创buildmyObject,但是当你有很多对象并且想要精确地控制时,这种types的对象创build/销毁变得有用当它们被创build和销毁时(例如,你的应用程序不会在整个生命周期内吸收1GB的RAM),而在Windowed环境中,这是非常必要的,因为你创build的对象(button,比如说) ,需要在任何特定function(甚至是类)的范围之外存在。