C语言之内存管理

时间:2022-10-31 01:30:16

原文出处:http://blog.csdn.net/jiaxiongxu/article/details/6613315

本文主要是以菜鸟的角度看C语言内存管理,分析malloc最基本的实现方法,如果已经知道malloc的实现方法的大鸟们,可以直接忽略本文了,呵呵。

在8086汇编时代里,是没有全局变量和局部变量之分的,通常的做法是:1、自己选定一片内存空间,用伪指令起个别名就当作全局变量来用。2、自己选一片内存空间作为栈,用push和pop操作栈就是操作局部变量了。

这样做法有很明显的问题——所有变量甚至栈的内存地址都是自己任意选定的,假如数据量庞大的时候,很容易搞不清哪些地址已经使用,哪些没有使用。假如自己定义的变量有地址重复或者和系统重要的变量地址重复,那么后果就不堪设想了(比野指针破坏力更大!)。

下面来谈谈C语言里面的内存管理。在C语言里,我们完全可以不用自己选定内存,试回想一下我们C语言编程时有几种定义变量的方式:1、在函数里定义临时变量。2、在函数外定义全局变量。3、定义指针指向用malloc申请的空间里等。是完全不用自己去选定内存,不用自己去考虑哪些内存地址使用没使用的吧~!(没用过汇编的同学们可能没有内存管理的概念,以为内存分配全都是系统做的事,其实很大一部分是语言设计者做的工作,甚至我们也可以自己来管理,这是C语言强大的体现^ ^)

接着解读:

1、函数里定义的临时变量是自动分配到栈里,随着函数的返回而自动释放(释放的意思是:告诉进程这个内存空间不再使用了,可以分配给其他变量)。

2、函数外定义的全局变量是在静态存储区域里分配的,静态存储区域就是编译时已经划分好的区域(反正就是专门放全局变量和静态变量的地方^ ^)在进程整个生命周期里一直都在,直到进程结束才被释放。

3、malloc申请的空间在堆里(堆和栈都是一种数据结构,不懂也没关系,知道它是专门用来放malloc这类动态分配的内存就行了^ ^),malloc这类动态分配是C语言内存管理最美妙的地方,他会自动在堆里寻找合适你想申请大小的内存区并返回,而且他知道哪些内存用过哪些没用过,绝对不会出现变量内存重叠的情况出现(除非你越界了-。-),而且当内存碎片很多、连续空间不足的时候malloc会移动内存将零碎的内存片组合成大的,厉害吧!malloc申请的空间一直都存在直到进程结束或者用free释放。最后,切忌,为了体现C语言高效的内存管理特性,使用malloc完之后必须要用free来释放申请的内存。


malloc如此美妙,那么他是怎样实现的呢?

我们定义指针p指向用malloc申请的内存,就可以想平时操作变量那样*地在这片内存空间上存取数据,释放时,调用free(p)就可以释放这片内存空间了。仔细一想,这里抛出了两个问题:1、free的参数p只是一个指针,如何知道p申请的内存空间大小呢? 2、根据前面所说释放的定义,free如何通知进程p这个内存空间不用了,以便下次malloc可以重复使用呢?


根据我们编程的经验,肯定在某个地方有专门的两个字段用于存储对应内存段的大小和是否被利用的标记位。(malloc在不同编程环境中,有不同的实现,但是主要思想还是一样的,本文讲的就是最基本最简单的一种实现方式。)

如上所说标志信息通常为:

[cpp] view plaincopyprint?
  1. struct mem_block_tag {  
  2.     int is_available;                   //标记此空间是否正在被利用  
  3.     size_t size;                        //此空间的大小  
  4. };  


那么标志信息应该放在什么地方才可以保证不影响malloc返回指针的正常使用呢?通常的做法是放在p指针指向的地址之前——好处是在开发者不越界的情况下永远不会修改到标志消息的。如图下图所示:

[cpp] view plaincopyprint?
  1. void* p1 = malloc(cSize1);  
  2. void* p2 = malloc(cSize2);  

C语言之内存管理


malloc返回开发者所需要的内存空间地址,并在此地址前加上标记头。如此每次调用malloc时,malloc就能根据标志头的is_available知道这片内存能不能用,根据size就能知道这片区域是否足够大并且同时定位到下一片内存空间(本标记头地址+本内存大小==下个内存标记头地址 罗)。

我们大概了解了malloc是怎么找到未使用合适大小的内存区(根据每段内存空间的标记头)。那么free函数的实现就自然明了了:

[cpp] view plaincopyprint?
  1. void free(void *ptr)  
  2. {  
  3.     struct mem_block_tag *pTag;  
  4.     pTag = ptr - sizeof(struct mem_block_tag); //找到标记头  
  5.     pTag->is_available = 1;                    //将空间标记为未使用,就等于告诉malloc这个内存空间不再使用了,可以分配给其他变量咯  
  6. }  

最后malloc得先拥有一片内存区域才能在其上进行内存管理。之前我们说过malloc申请的内存是在堆上的,怎么找到未用的堆内存呢?

linux里提供标准C函数sbrk来增加程序可用数据段内存空间,返回值是该新内存空间的首地址。

注意:某个堆内存要用sbrk函数增加后才能使用,不然是不能访问的。(sbrk的作用实际上就是将虚拟内存映射到内存里,想更深入的了解自行google ^ ^)

比较一下下面两段代码:

[cpp] view plaincopyprint?
  1. #include <stdio.h>   
  2. #include <malloc.h>   
  3. int main()  
  4. {  
  5.     int* p1=(int*)malloc(0);  
  6.     printf("%d\n",*p1);        //这里先不管*p1是什么值,只要p1内存可读,就肯定会输出一个值  
  7.     printf("Hello World");  
  8.     return 0;  
  9. }  
[cpp] view plaincopyprint?
  1. 输出结果:  
  2. 0                     
  3. Hello World  

[cpp] view plaincopyprint?
  1. #include <stdio.h>   
  2. #include <unistd.h>   
  3. int main()  
  4. {  
  5.     int* p1=(int*)sbrk(0);  
  6.     printf("%d\n",*p1);       //这里先不管*p1是什么值,只要p1内存可读,就肯定会输出一个值   
  7.     printf("Hello World");  
  8.     return 0;  
  9. }  
[cpp] view plaincopyprint?
  1. 没有输出结果,程序运行到printf("%d\n",*p1);中断退出,证明p1地址的内存不可读。  

因为sbrk返回是新增内存的首地址,而这里用sbrk(0)增加0大小的内存,即返回的地址是没用sbrk增加的,由此可证明,对于进程来讲,没用sbrk增加的堆内存区域是不可读的。

而且用sbrk增加的内存空间都是在原来空间上往后加,是连续的一片的。

所以malloc实现的策略是,在最开始的时候用sbrk先取得一片内存空间用来进行内存管理。之后每次调用malloc先检查已有的堆内存有没有足够大可用的,若没有则再用sbrk去增加。

从这里可知道,必须还有三个全局变量分别标记:是否初始化(是否已用sbrk取得最处内存空间)、已有堆内存的首地址,已有堆内存的尾地址。

[cpp] view plaincopyprint?
  1. int has_initialized = 0;  
  2. void *managed_memory_start;  
  3. void *managed_memory_end;  

在windows里并没有提供sbrk函数(sbrk是标准的C库函数呢-。-)

通常的做法是先用系统提供的malloc申请一片大空间,再在上面进行内存管理实现自己malloc函数。

有同学会说:既然已经提供了malloc为什么还要自己实现呢?...

第一,学习了解C语言的内存管理机制。

第二,系统提供的malloc是万能的什么情况都能用,但对于具体某个程序不一定是最优的,某些情况下可能就需要针对自己程序量身定做一套内存管理机制了。


更具体实现的细节这篇文章《malloc的实现原理学习》已经非常详细了,这里就不重复了。


大家快点去实现吧。^ ^