malloc()的实现很简单。它首先会扫描之前由 free()所释放的空闲内存块列表,以求找到尺寸大于或等于要求的一块空闲内存。(取决于具体实现,采用的扫描策略会有所不同。例如,first-fit 或 best-fito。)如果这一内存块的尺寸正好与要求相当,就把它直接返回给调用者。如果是一块较大的内存,那么将对其进行分割,在将一块大小相当的内存返回给调用者的同时,把较小的那块空闲内存块保留在空闲列表中。 如果在空闲内存列表中根本找不到足够大的空闲内存块,那么 malloc()会调用 sbrk()以分配更多的内存。为减少对 sbrk()的调用次数,malloc()并未只是严格按所需字节数来分配内存,而是以更大幅度(以虚拟内存页大小的数倍)来增加 program break,并将超出部分置于空闲内存列表。
当 free()将内存块置于空闲列表之上时,是如何知晓内存块大小的?这是通过一个小技巧来实现的。当 malloc()分配内存块时,会额外分配几个字节来存放记录这块内存大小的整数值
malloc()返回的内存块
当将内存块置于空闲内存列表(双向链表)时,free()会使用内存块本身的空间来存放链表指针,将自身添加到列表中
空闲列表中的内存块
随着对内存不断地释放和重新分配,空闲列表中的空闲内存会和已分配的在用内存混杂在一起
包含有已分配内存和空闲内存列表的堆
应该认识到,C 语言允许程序创建指向堆中任意位置的指针,并修改其指向的数据,包括由 free()和 malloc()函数维护的内存块长度、指向前一空闲块和后一空闲块的指针。例如,假设经由一个错误指针,程序无意间增加了冠于一块已分配内存的长度值,并随即释放这块内存,free() 因之会在空闲列表中记录下这块长度失真的内存。随后,malloc()也许会重新分配这块内存,从而导致如下场景:程序的两个指针分别指向两块它认为互不相干的已分配内存,但实际上这两块内存却相互重叠。至于其他的出错情况则数不胜数。
要避免这类错误,应该遵守以下规则。
- 分配一块内存后,应当小心谨慎,不要改变这块内存范围外的任何内容。
- 释放同一块已分配内存超过一次是错误的。
- 若非经由 malloc 函数包中函数所返回的指针,绝不能在调用 free()函数时使用。
- 在编写需要长时间运行的程序时,出于各种目的,如果需要反复分配内存,那么应当确保释放所有已使用完毕的内存。如若不然,堆将稳步增长,直至抵达可用虚拟内存的上限,在此之后分配内存的任何尝试都将以失败告终。这种情况被称之为“内存泄漏”。