内存分配

上一篇文章我们分析了C语言中的五种存储类(Storage Class),其实五种不同的存储类(Storage Class) 就代表着C语言中五种不同的内存管理规则,除了这五种"内置"的内存管理规则,C语言还允许由程序员来管理内存,而不是采用这种预先规定好的内存规则,这给程序员带来了很大的自由度,使得程序员能够直接操纵内存的分配和管理。

但是C语言中的这种高度自由也有着隐患,一旦程序员在内存管理上出现了问题,那么整个程序就会出现问题,所以在对内存进行管理的时候,要尤其小心。

通常情况下,每当定义一个变量(只可能是五种存储类中的一种),系统就会为这个变量分配内存空间,同时用这个变量名来标识内存中的数据,这个变量所占用的空间是由系统来维护的,你不需要在意它是否被回收(事实上它一定会在某个时间点被回收)。

除了上述由系统维护的内存,C语言还可以直接申请和管理内存,主要是通过mallocfree库函数来完成的。

malloc函数

malloc函数的原型为:

:::c
void *malloc(long NumBytes);

malloc函数的参数NumBytes指明了向系统请求的内存字节数,malloc函数接受参数之后,会自动在内存中寻找一块满足申请大小的连续区域块,但是由于并没有一个变量来标识这块内存块,因此malloc找到合适的内存块之后,就会返回这块内存块的起始地址,也即第一个字节的地址。因此,需要一个指针类型的变量来接受(存储)这个内存块的起始地址。

在上述的函数原型中,可以看到,malloc函数返回值的类型是一个void类型的指针,这是因为C语言它并不知道申请的这片内存将会用来做什么,它不能确定其类型信息,而void类型的指针1可以通过强制类型转换将其转成任意类型的指针,这就能够避免不必要的类型转换问题了。

通常在实际使用的时候,都会显示地对void指针进行类型转换,以增加程序的可读性,如下所示:

:::c
double * ptd;
ptd = (double *)malloc(30 * sizeof(double))

上述代码中的强制类型转换(double *)在C语言中并不是必须的,但是在C++ 中却是必须的,因此使用显式的强制类型转换,既可以增加程序的可读性,还使得程序移植到C++ 更容易。

如果malloc没有找到合适的内存块,则将会返回一个空指针。

需要注意的是,由malloc分配的内存空间是不遵循之前分析的五种存储类(Storage Class)的内存管理规则的,一旦通过malloc分配得到了内存空间,这段内存空间除非通过free函数进行释放,否则这段内存空间将会一直被占用。但是,用来存储这段内存空间的首地址的指针却是一个普通的变量,这个指针变量必定属于五种存储类(Storage Class)中的一种,因此如果在函数内部使用malloc函数分配得到了一片内存空间,它的首地址存放在指针变量ptd中,此时,如果程序员忘记在函数结束前用free函数释放这一段内存空间且ptd是属于的自动存储类的话,那么函数一旦结束,ptd变量也就消失了,但是之前申请的那段内存空间还是存在的,一直处于被占用的状态,而ptd变量存储的是那块内存空间的首地址,因此ptd一旦消失,我们就"丢失"了那段内存空间,虽然它一直存在,但是我们永远不能访问到它了,因为我们已经丢失了这段内存的首地址。这就是所谓的内存泄露

free函数

一般来说,对应于每个malloc函数的调用,都应该调用一次free函数来将其占用的内存空间释放掉。

free函数的原型为:

:::c
void free(void *FirstByte)

free接受一个指针参数,这个指针指向了需要被释放的内存空间的首地址。

需要注意的是,free函数释放的是参数指针指向的内存空间而不是指针变量本身,因此,调用了free函数之后,原来的指针还是指向着原来的内存空间的首地址,但此时内存中的数据是未定义的,是垃圾数据,因此,在实践中一个比较好的习惯是,将已经被释放了空间的指针赋值为NULL,防止引用到错误的内存空间。

calloc函数

calloc函数也是用来申请内存空间的,函数原型为:

:::c
void *calloc(unsigned n, unsigned size);

它的功能几乎和malloc函数一样,但是细节部分有所不同:它接受两个参数nsize,分别代表所需要内存单元的数目和每个单元以字节计的大小。另外它和malloc还有一个区别是,calloc会自动为申请的内存空间自动填充为0,而malloc不会,malloc申请得到的内存空间中都是一些垃圾数据。

总结

之前分析过的五种存储类(Storage Class)再加上我们今天分析的malloc函数,也许有人会被搞糊涂了,这里我们总结一下C语言中的内存模型,我们可以将程序的可用内存分成三个独立的部分:

  1. 具有静态存储时期的变量空间,所有具有静态存储时期的变量都属于这部分空间,这些变量从定义开始就一直存在,直到程序结束。
  2. 自动变量存在的变量空间(栈空间),栈空间中的变量都是定义在代码块中的,当代码块结束时,变量也就被清除。C语言是用一个栈结构来管理这些变量,所以当消除自动变量时,是按照定义这些变量的顺序的反序进行的。
  3. 动态分配的内存空间(堆空间),堆空间是提供给malloc函数使用的,所有通过malloc获得的内存空间都是来自这一部分,只有当调用free函数时,这部分内存才会被释放,否则将会一直存在。

尽管mallocfree函数给程序员在内存管理上带来的极大的便利,但它还是存在隐患的,这里的隐患并不是指之前提到的内存泄露内存泄露是由于程序员的粗心大意才导致的,是程序上的错误,但是我这里想说的隐患其实是程序员无法控制的。这个隐患就是,当大量使用mallocfree函数之后,堆内存空间中会产生大量碎片区域,当你下一次申请的内存空间大小大于每一个碎片的大小,但是又小于这些碎片的总和时,即使内存中总的空闲空间是足够的,但是你并不能获得这些空间,因为这些空间是呈碎片状分布在内存中的,这就造成了内存空间的极大浪费。

解决内存碎片的技巧有很多,但是都不能完全解决这个问题,只能在很大程度上缓解这个问题。比较常用的方式是,在程序一开始就申请一块很大的空间,这个空间称为内存池,之后程序中所有需要的内存空间都从这个内存池中获取,这就在很大程度上避免了多次的mallocfree,这也使得内存碎片出现的概率降低了。但是这种技巧需要很强的内存操纵经验,一不小心就会出现内存错误。

参考资料

  • 《C Primer Plus》

  1. void类型的指针可以理解为一种"通用指针" 

Share on: TwitterFacebookEmail


Flyaway is the owner of this blog.
Comments

So what do you think? Did I miss something? Is any part unclear? Leave your comments below

comments powered by Disqus

Published

Category

programming-language

Tags

Contact