一、函数介绍
以下四个函数都包含在头文件< stdlib.h >
1.malloc
函数原型:void *malloc(size_t size);
作用:从堆空间申请内存
函数参数:需要申请的空间大小(字节数)
返回值:申请成功则返回一个指向申请到的内存的指针,失败则返回NULL
2.calloc
函数原型:void *calloc(size_t num,size_t size)
作用:从堆空间申请内存,并把申请到的内存初始化为0
函数参数:第一个参数是需要申请多少块内存,第二个参数是每块内存的大小(字节数)
返回值:申请成功则返回一个指向申请到的内存的指针,失败则返回NULL
3.realloc
函数原型:void* realloc(void *ptr,size_t size)
作用:用于修改一个原先已经分配的内存块的大小
函数参数:第一个参数是指向已分配到的内存的指针,第二个参数是重新设定内存的大小(字节数)
返回值:申请成功则返回一个指向申请到的内存的指针,失败则返回NULL
注意:如果relloc函数的第一个参数是NULL,那么它的行为就和malloc相同
4.free
函数原型:void free(void *ptr)
作用:释放从堆空间申请到的内存
二、malloc的实现机理和内存池
从表面上来看,我们每次使用malloc的时候,都是从堆空间“要”了一块空间,好像是系统提供了相应的系统调用,可以用malloc函数使用这个系统调用去申请内存。但是实际上,这样做的性能很差,因为每次申请空间和释放空间都会进行系统调用,而系统调用的开销是比较大的,频繁的堆操作会严重影响程序的性能。
所以这里采用另一种方法,使用malloc的时候,系统从堆上分配了一块足够大的空间,然后以后的内存操作都是在这块空间上执行,由malloc来管理这块空间,当这块空间不足时,再向系统申请,这样就可以避免频繁的系统调用。
分配算法
如果你是个有心人的话,相信你会在很多书上看到这句话:栈内存的分配类似于数据结构中的栈结构,堆内存的分配类似于数据结构中的链表。这句话说的没错,以前堆内存的管理就类似于双向链表。
对上面那些内存块的管理采用双向链表的方式,它们通过指针连接。
从这个图我们还可以的得出的一个结论就是:在使用malloc的时候,尽量一次申请一块足够大的空间。因为如果你频繁申请那些比较小的空间,那么你申请的空间附带的额外开销(额外内存空间)就会很大。
所以整个堆区就会出现下面这种布局:
其中free会将连续的空闲块合并为一个大的空闲块(类似操作系统内存管理的伙伴系统)
malloc和free的工作就是对已有内存块的拆分和合并,并没有频繁的向操作系统申请内存,这样就大大提高了内存分配的效率。
但是这样又引入了新的问题:
- 形成很多内存碎片
- 频繁分配和释放内存造成链表过长,导致遍历时间增加
- 容易出现越界读写
针对这些问题,后来的内存管理又提出了内存池的概念
内存池
不管具体的分配算法是怎样的,为了减少系统调用,减少物理内存碎片,malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,这就是内存池。
内存池的研究重点不是向操作系统申请内存,而是对已申请到的内存的管理,这涉及到非常复杂的算法,是一个永远也研究不完的课题,除了C标准库自带的 malloc(),还有一些第三方的实现,比如 Goolge 的 tcmalloc 和 jemalloc。
内存池的详细讲解参考:
内存池技术介绍