1. malloc和free

C函数库提供了两个函数,mallocfree,分别用于执行动态内存分配和释放。这些函数维护一个可用内存池。当一个程序另外需要一些内存时,它就调用malloc函数,malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。如果对这块内存进行初始化非常重要,要么自已动手对它进行初始化,要么使用calloc函数(在下一节描述)。当一块以前分配的内存不再使用时,程序调用free函数把它归还给内存池供以后之需。

这两个函数的原型如下所示,它们都在头文件stdlib.h中声明。

void *malloc( size_t size );
void free( void *pointer );

malloc的参数就是需要分配的内存字节(字符)数 。如果内存池中的可用内存可以满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针。

malloc所分配的是一块连续的内存。例如,如果请求它分配100个字节的内存,那么它实际分配的内存就是100个连续的字节,并不会分开位于两块或多块不同的内存。同时,malloc实际分配的内存有可能比你请求的稍微多一点。但是,这个行为是由编译器定义的,所以你不能指望它肯定会分配比你的请求更多的内存。

如果内存池是空的,或者它的可用内存无法满足你的请求,会发生什么情况呢?在这种情况下,malloc函数向操作系统请求,要求得到更多的内存,并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。因此,对每个从malloc返回的指针都进行检查,确保它并非NULL是非常重要的。

free的参数必须要么是NULL,要么是一个先前从malloccallocrealloc(稍后描述)返回的值。向free传递一个NULL参数不会产生任何效果。

malloc又是如何知道所请求的内存需要存储的是整数、浮点值、结构还是数组呢?它并不知情——malloc返回一个类型为void *的指针,正是缘于这个原因。标准表示一个void *类型的指针可以转换为其他任何类型的指针。但是,有些编译器,尤其是那些老式的编译器,可能要求在转换时使用强制类型转换。

对于要求边界对齐的机器,malloc所返回的内存的起始位置将始终能够满足对边界对齐要求最严格的类型的要求。

2. calloc和realloc

另外还有两个内存分配函数,callocrealloc。它们的原型如下所示:

void *calloc( size_t num_elements, size_t element_size );
void realloc( void *ptr, size_t new_size );

calloc也用于分配内存。malloccalloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。这个初始化常常能带来方便,但如果程序只是想把一些值存储到数组中,那么这个初始化过程纯属浪费
时间。callocmalloc之间另一个较小的区别是它们请求内存数量的方式不同。calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值,它能够计算出总共需要分配的内存。

realloc函数用于修改一个原先已经分配的内存块的大小。使用这个函数,可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存便被拿掉,剩余部分内存的原先内容依然保留。

如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针。

最后,如果realloc函数的第1个参数是NULL,那么它的行为就和malloc一模一样。

3. 使用动态分配的内存

这里举一个例子:

int *pi;
pi = malloc(100);
if (pi == NULL)
{
	printf("Out of memory!\n");
	exit(1);
}

符号NULL定义于stdio.h,它实际上是字面值常量0。它在这里起着视觉提醒器的作用,提醒我们进行测试的值是一个指针而不是整数。

如果内存分配成功,那么我们就拥有了一个指向100个字节的指针。在整型为4个字节的机器上,这块内存将被当作25个整型元素的数组,因为pi是一个指向整型的指针。

如果目标就是获得足够存储25个整数的内存,这里有一个更好的技巧来实现这个目的。

pi = malloc( 25 * sizeof( int ) );

这个方法更好一些,因为它是可移植的。即使是在整数长度不同的机器上,它也能获得正确的结果。

既然已经有了一个指针,那么该如何使用这块内存呢?当然,可以使用间接访问和指针运算来访问数组的不同整数位置,下面这个循环就是这样做的,它把这个新分配的数组的每个元素都初始化为0:

int *pi2, i;
pi2 = pi;
for (i = 0; i < 25; ++i)
{
	*pi2++ = 0;
}

不仅可以使用指针,也可以使用下标。下面的第2个循环所执行的任务和前面一个相同。

int *pi2, i;
pi2 = pi;
for (i = 0; i < 25; ++i)
{
	pi2[i] = 0;
}

4. 常见的动态内存错误

在使用动态内存分配的程序中,常常会出现许多错误。这些错误包括对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存、试图释放一块动态分配的内存的一部分以及一块动态内存被释放之后被继续使用。

动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配。这种问题显而易见:被访问的内存可能保存了其他变量的值。对它进行修改将破坏那个变量,修改那个变量将破坏存储在那里的值。这种类型的bug非常难以发现。下面的程序展现了一种技巧,可以很可靠地进行这个错误检查。MALLOC宏接受元素的数目能及每种元素的类型,计算总共需要的内存字节数,并调用alloc获得内存。alloc调用malloc并进行检查,确保返回的指针不是NULL。

这个方法最后一个难解之处在于第1个非比寻常的#define指令。它用于防止由于其他代码块直接塞入程序而导致的偶尔直接调用malloc的行为。增加这个指令以后,如果程序偶尔调用了malloc,程序将由于语法错误而无法编译。在alloc中必须加入#undef指令,这样它才能调用malloc而不至于出错。

  • alloc.h:错误检查分配器:接口
/*
** 定义一个不易发生错误的内存分配器。
*/
#include < stdlib.h>
#define malloc //不要直接调用malloc!
#define MALLOC(num,type) (type *)alloc( (num) * sizeof(type) )
extern void *alloc( size_t size );
  • alloc.c:错误检查分配器:实现
/*
** 不易发生错误的内存分配器的实现
*/
#include <stdio.h>
#include "alloc.h"
#undef malloc
void *
alloc( size_t size )
{
void *new_mem;
/*
** 请求所需的内存,并检查确实分配成功
*/
new_mem = malloc( size );
if( new_mem == NULL ){
printf( "Out of memory!\n" );
exit( 1 );
}
return new_mem;
}
  • a_client.c:使用错误检查分配器
/*
** 一个使用很少引起错误的内存分配器的程序
*/
#include "alloc.h"
void
function()
{
 int *new_memory;
/*
** 获得一串整型数的空间
*/
new_memory = MALLOC( 25, int );
/* ... */
}

动态内存分配的第二大错误来源是操作内存时超出了分配内存的边界。例如,如果得到一个25个整型的数组,进行下标引用操作时如果下标值小于0或大于24将引起两种类型的问题。这种问题就不是那么明显。在malloc和free的有些实现中,它们以链表的形式维护可用的内存池。对分配的内存之外的区域进行访问可能破坏这个链表,这有可能产生异常,从而终止程序

当使用free时,可能出现各种不同的错误。传递给free的指针必须是一个从malloccallocrealloc函数返回的指针。传给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。试图释放一块动态分配内存的一部分也有可能引起类似的问题,像下面这样:

/*
** 获得10个整型内存空间
*/
pi = malloc( 10 * sizeof(int)) ;
/*
** 释放最后5个整型类型的内存空间,保留前5个内存空间
*/
free(pi +5);
}

释放一块内存的一部分是不允许的。动态分配的内存必须整块一起释放。但是,realloc函数可以缩小一块动态分配的内存,有效地释放它尾部的部分内存。

必须小心在意,不要访问已经被free函数释放了的内存。这个警告看上去很显然,但这里仍然存在一个很微妙的问题。假定对一个指向动态分配的内存的指针进行了复制,而且这个指针的几份拷贝散布于程序各处。无法保证当使用其中一个指针时它所指向的内存是不是已被另一个指针释放。另一方面,必须确保程序中所有使用这块内存的地方在这块内存被释放之前停止对它的使用。

内存泄漏

当动态分配的内存不再需要使用时,它应该被释放,这样它以后可以被重新分配使用。分配内存但在使用完毕后不释放将引起内存泄漏(memory leak)。在那些所有执行程序共享一个通用内存池的操作系统中,内存泄漏将一点点地榨干可用内存,最终使其一无所有。要摆脱这个困境,只有重启系统。

其他操作系统能够记住每个程序当前拥有的内存段,这样当一个程序终止时,所有分配给它但未被释放的内存都归还给内存池。但即使在这类系统中,内存泄漏仍然是一个严重的问题,因为一个持续分配却一点不释放内存的程序最终将耗尽可用的内存。此时,这个有缺陷的程序将无法继续执行下去,它的失败有可能导致当前已经完成的工作统统丢失。

11-11 05:07