堆与栈是C程序中非常重要的内存空间,它们都是操作系统在程序运行是为其分配的一部分内存空间,只不过他们在实现原理和作用上不同罢了。前面章节中提到过堆。在C语言中动态分配内存通常是通过堆来实现的。我们知道,当程序并操作系统载入内存后,被称作进程,进程可以使用操作系统为其分配的堆栈空间。假设程序在没有申请动态内存空间时堆的大小为1024B(这个数值很小,只是为了说明问题,实际上堆的内存空间很大、很充足),它在内存中的结构如下:
操作系统会分配一个指针变量p指向堆可用空间的起始位置,这个地址第1个4字节存放了下一个空闲空间的地址,第2个字节存放了这个空闲空间的大小。当使用malloc()函数来申请内存时系统会从堆的头部p指针开始查找,找到一个符合申请需要的空闲内存空间,并返回其首地址。例如申请4个字节的内存空间:
void *p1 = malloc(4);
申请内存后堆的内容如下:
操作系统的内存分配算法如下:
接下来再进行内存申请:
void *p2 = malloc(8);
结果返回的是0xc,堆图如下:
接下来再进行内存申请:
void *p3 = malloc(4);
结果返回的是0x14,堆图如下:
当调用free函数进行内存释放时,操作系统会根据对free函数的传入参数进行处理,查找这个地址的前4字节,因为申请内存时返回的地址前4个字节中记录着申请的内存大小。于是操作系统将按此大小释放内存空间。例如:
free(p2);
内存堆图如下:
注意,当执行内存释放操作时被释放的内存中的前8个字节会有特殊的记录,前4字节记录着下一处空闲空间的地址,即0x1c,第2个4字节记录着这一块空闲空间的大小0x8字节。而对于0x1c处的地址也将被记录为下一个空闲空间地址即0x8。
我们接下来再释放p3所占用的内存:
free(p3);
内存堆图如下:
同样,操作系统释放了p3之后0x14处的内存地址被释放,但这一块内存也变成了空闲空间,它的下一个空闲空间地址为0x1c,而0x1c处的下一个空闲空间为0x8,而0x8处的下一个空闲空间为0x14。
另外,当操作系统释放了一块内存空间之后如果出现了两个相临的空闲空间,有的操作系统会将其合并成一个整体块,而大多数操作系统并不会这样处理。
当进程动态内存释放时,通常是将内存地址传入free函数,操作系统会认为这个地址的前4字节存放了申请内存的大小,并按这个大小进行内存释放操作。如果我们在内存释放时,传入一个异常的地址呢?比如:
#include <stdlib.h> int main(int argc, char **args) { void *p = malloc(8); free(p + 4); return 0; }
这条语句是合法的,但是在程序运行时会产生非常混乱的结果:
*** Error in `./main': free(): invalid pointer: 0x08fb900c ***
当操作系统为程序释放内存时,操作系统会认为这个地址的前4字节就是申请内存的大小,无论这个地址是不是当初申请的那个。所以申请和释放内存是应格外的小心谨慎。一个小小的疏忽就可能导致整个程序的崩溃。
Copyright © 2015-2023 问渠网 辽ICP备15013245号