在学习C/C++的过程中,malloc、realloc和free这些函数,都是内存操作方面最基本的函数,使用非常广泛。今天本文主要是浅显地介绍一点这些函数的原理。
Realloc
先来介绍一下realloc,因为这个函数实际上是malloc和free的二次封装。
void *realloc(void *__ptr, size_t __size)
Realloc函数接受2个参数:一个指针和一个size,根据参数不同,做的事情也不同:
1、当ptr是一个空指针时,方法近似于malloc(不推荐使用);
2、当ptr非空,而size为0时,方法近似于free(不推荐使用);
3、当ptr非空,而size不为0时,方法是重新为ptr分配新的内存区域,尺寸为size。
其实以现代程序设计的眼光来看,realloc将几种不同的操作混为一个接口并不是一种很优秀的设计。如果使用不当的话,也会造成内存溢出或崩溃之类的问题。
一般来说,使用realloc时,比较容易出现问题的是这么些场景:
1、先对指针进行free,再调用realloc时会崩溃,或者先调用了realloc(ptr,0)再调用realloc或free,可能会有未知问题。
在这里,很多中文网络上的文章有一个误区是,认为realloc的size参数为0时函数的表现完全等于free。事实上,这是不确切的,而是和操作系统的malloc库实现有关。举个例子:
在作者使用的Mac电脑中,上面这段程序的输出是下面这个样子的:
先调用realloc(a,0)再调用realloc(a,5)程序表现完全正常,不会崩溃,只是地址会重新分配,然而如果先调用free再调用realloc则会崩溃。
这也是前面为什么不推荐使用realloc(ptr,0)来代替free的原因,这个函数的表现与系统库实现有关,可能会导致未知bug。在实际应用中,当realloc的第二个参数是变量的时候,一定要检测该变量的值是否为0。
2、没有考虑realloc失败,返回值为NULL的情况,直接赋值导致指针指向了NULL,而之前的内存成为无法回收的野指针。
就拿我们前面写的这句代码来说,其实它是有问题的,如果realloc失败,这段代码相当于a=NULL,这时a之前指向的地址就无法回收了。因此,更严格的写法应该是这个样子的:
Malloc和free
Malloc和free这对好基友才是本文的重点。在之前的文章中我们说过,内存操作的效率远低于纯算术运算。今天本文粗浅地介绍一些malloc和free的实现原理,就能理解其中的原因。
在ANSI之前,没有void*类型,malloc的返回值是char*,而在ANSI以后的C和C++中,malloc的返回值是void*,可以通过强制转换为其他类型的指针。不过需要注意的是,转换为非char类型时,要注意malloc的长度得是对应类型的整数倍,否则最后一个数由于位数不满,会出奇怪的未知问题。
那么,在调用malloc时,系统库到底做了什么事情呢?
在应用程序使用内存的过程中,会产生很多内存碎片,如下图所示:
Malloc对空闲内存的管理,通过空闲链表来实现。最基础的空闲链表每一个节点包含两个字段:下一段空闲内存的地址和当前空闲段的大小。对上图的情况,对应的空闲链表的基本内容是这个样子的:
在执行malloc时,基础的操作流程是扫描空闲链表,寻找尺寸合适的空闲块,找到之后将空闲块的地址返回,同时记录下malloc申请的尺寸。如果找不到则向操作系统申请一块新的内存加入到空闲链表中然后做找到的处理。做完这些操作之后,更新空闲链表。
从这个流程我们还可以看出一点,那就是虽然malloc(n)申请的内存是n,但实际消耗的内存并不是只有n,而是n+size_t,即n加上一个用于记录尺寸的数值。这也解释了另外一个问题,为什么malloc需要传尺寸,而free不需要,因为尺寸已经在头信息中记录下来了。
在执行free时,系统会将要free的这块区域标记为空闲可用,但并不会真正清除其中的值。也就是说free了一个指针然后马上去取之前地址中的值,这个值还存在。如下例所示:
对C/C++来说,这块地址标记为空闲后,下次再有代码申请到这一块堆内存并写入新值后,才会真正把原值覆盖掉。当然,在实际应用中并不推荐这样的代码。
同时,free还会做的一件事情是更新空闲链表,在更新时,如果检测到这段内存前后有连续空闲内存的话,还会将这几个相邻地址的链表节点进行合并,以提高遍历效率。