内存分配
在上一篇文章我们分析了C语言中的五种存储类(Storage Class),其实五种不同的存储类(Storage Class) 就代表着C语言中五种不同的内存管理规则,除了这五种"内置"的内存管理规则,C语言还允许由程序员来管理内存,而不是采用这种预先规定好的内存规则,这给程序员带来了很大的自由度,使得程序员能够直接操纵内存的分配和管理。
但是C语言中的这种高度自由也有着隐患,一旦程序员在内存管理上出现了问题,那么整个程序就会出现问题,所以在对内存进行管理的时候,要尤其小心。
通常情况下,每当定义一个变量(只可能是五种存储类中的一种),系统就会为这个变量分配内存空间,同时用这个变量名来标识内存中的数据,这个变量所占用的空间是由系统来维护的,你不需要在意它是否被回收(事实上它一定会在某个时间点被回收)。
除了上述由系统维护的内存,C语言还可以直接申请和管理内存,主要是通过malloc
和free
库函数来完成的。
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
函数一样,但是细节部分有所不同:它接受两个参数n
和size
,分别代表所需要内存单元的数目和每个单元以字节计的大小。另外它和malloc
还有一个区别是,calloc
会自动为申请的内存空间自动填充为0,而malloc
不会,malloc
申请得到的内存空间中都是一些垃圾数据。
总结
之前分析过的五种存储类(Storage Class)再加上我们今天分析的malloc
函数,也许有人会被搞糊涂了,这里我们总结一下C语言中的内存模型,我们可以将程序的可用内存分成三个独立的部分:
- 具有静态存储时期的变量空间,所有具有静态存储时期的变量都属于这部分空间,这些变量从定义开始就一直存在,直到程序结束。
- 自动变量存在的变量空间(栈空间),栈空间中的变量都是定义在代码块中的,当代码块结束时,变量也就被清除。C语言是用一个栈结构来管理这些变量,所以当消除自动变量时,是按照定义这些变量的顺序的反序进行的。
- 动态分配的内存空间(堆空间),堆空间是提供给
malloc
函数使用的,所有通过malloc
获得的内存空间都是来自这一部分,只有当调用free
函数时,这部分内存才会被释放,否则将会一直存在。
尽管malloc
和free
函数给程序员在内存管理上带来的极大的便利,但它还是存在隐患的,这里的隐患并不是指之前提到的内存泄露,内存泄露是由于程序员的粗心大意才导致的,是程序上的错误,但是我这里想说的隐患其实是程序员无法控制的。这个隐患就是,当大量使用malloc
和free
函数之后,堆内存空间中会产生大量碎片区域,当你下一次申请的内存空间大小大于每一个碎片的大小,但是又小于这些碎片的总和时,即使内存中总的空闲空间是足够的,但是你并不能获得这些空间,因为这些空间是呈碎片状分布在内存中的,这就造成了内存空间的极大浪费。
解决内存碎片的技巧有很多,但是都不能完全解决这个问题,只能在很大程度上缓解这个问题。比较常用的方式是,在程序一开始就申请一块很大的空间,这个空间称为内存池,之后程序中所有需要的内存空间都从这个内存池中获取,这就在很大程度上避免了多次的malloc
和free
,这也使得内存碎片出现的概率降低了。但是这种技巧需要很强的内存操纵经验,一不小心就会出现内存错误。
参考资料
- 《C Primer Plus》
-
void
类型的指针可以理解为一种"通用指针" ↩