11.5 常见的动态内存错误
在使用动态内存分配的程序中,常常会出现许多错误。这些错误包括对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存、试图释放一块动态分配的内存的一部分,以及一块动态内存被释放之后被继续使用。
忘记检查所请求的内存是否成功分配
动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配。程序11.1展现了一种技巧,可以很可靠地进行这个错误检查。MALLOC宏接受元素的数目以及每种元素的类型,计算总共需要的内存字节数,并调用alloc获得内存。alloc调用malloc并进行检查,确保返回的指针不是NULL。
/* ** 定义一个不易发生错误的内存分配器。 */ #include <stdlib.h> #define malloc /*不要直接调用malloc!*/ #define MALLOC(num,type) (type *)alloc( (num) * sizeof(type) ) extern void *alloc( size_t size );
程序11.1a 错误检查分配器:接口 alloc.h
/* ** 不易发生错误的内存分配器的实现。 */ #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; }
程序11.1b 错误检查分配器:实现 alloc.c
/* ** 一个使用很少引起错误的内存分配器的程序。 */ #include "alloc.h" void function() { Int *new_memory; /* ** 获得一串整型数的空间。 */ new_memory = MALLOC( 25, int ); /* ... */ }
程序11.1c 使用错误检查分配器 a_client.c
这个方法最后一个难解之处在于第一个非比寻常的#define指令。它用于防止由于其他代码块直接塞入程序而导致的偶尔直接调用malloc的行为。增加这个指令以后,如果程序偶尔调用了malloc,程序将由于语法错误而无法编译。在alloc中必须加入#undef指令,这样它才能调用malloc而不至于出错。
批注:和C++的#ifndf有点类似,不过那个我也没太弄懂,在第十四章,到时候再仔细学习下。
链表的破坏
第二种问题不是那么明显。在malloc和free的有些实现中,它们以链表的形式维护可用的内存池。对分配的内存之外的区域进行访问可能破坏这个链表,这有可能产生异常,从而终止程序。
当一个使用动态内存分配的程序失败时,人们很容易把问题的责任推给malloc和free函数。但它们实际上很少是罪魁祸首。事实上,问题几乎总是出在你自己的程序中,而且常常是由于访问了分配内存以外的区域而引起的。
使用free的错误
当使用free时,可能出现各种不同的错误。传递给free的指针必须是一个从malloc、calloc或realloc函数返回的指针。传给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。试图释放一块动态分配内存的一部分也有可能引起类似的问题,像下面这样:
释放一块内存的一部分是不允许的。动态分配的内存必须整块一起释放。但是,realloc函数可以缩小一块动态分配的内存,有效地释放它尾部的部分内存。
最后,不要访问已经被free函数释放了的内存。这个警告看上去很显然,但这里仍然存在一个很微妙的问题。假定你对一个指向动态分配的内存的指针进行了复制,而且这个指针的几份副本散布于程序各处。当使用其中一个指针时,你无法保证它所指向的内存是不是已被另一个指针释放。另一方面,必须确保程序中所有使用这块内存的地方在这块内存被释放之前停止对它的使用。
内存泄漏
当动态分配的内存不再需要时,应该被释放,这样它以后可以被重新分配使用。分配内存但在使用完毕后不释放将引起内存泄漏(memory leak)。在那些所有执行程序共享一个通用内存池的操作系统中,内存泄漏将一点点地榨干可用内存,最终使其一无所有。要摆脱这个困境,只有重启系统。
其他操作系统能够记住每个程序当前拥有的内存段,这样当一个程序终止时,所有分配给它但未被释放的内存都归还给内存池。但即使在这类系统中,内存泄漏仍然是一个严重的问题,因为一个持续分配却一点不释放内存的程序最终将耗尽可用的内存。此时,这个有缺陷的程序将无法继续执行下去,它的失败有可能导致当前已经完成的工作统统丢失。
11.6 内存分配实例
动态内存分配一个常见的用途就是为那些长度在运行时才知的数组分配内存空间。程序11.2读取一列整数,并按升序排列它们,最后打印这个列表。
批注:我们当时上课讲的好像也是这个例子?
/* ** 读取、排序和打印一列整型值。 */ #include <stdlib.h> #include <stdio.h> /* ** 该函数由'qsort'调用,用于比较整型值。 */ int compare_integers( void const *a, void const *b ) { register int const *pa = a; register int const *pb = b; return *pa > *pb ? 1 : *pa < *pb ? -1 : 0; } int main() { int *array; int n_values; int i; /* ** 观察共有多少个值。 */ printf( "How many values are there? " ); if( scanf( "%d", &n_values ) != 1 || n_values <= 0 ){ printf( "Illegal number of values.\n" ); exit( EXIT_FAILURE ); } /* ** 分配内存,用于存储这些值。 */ array = malloc( n_values * sizeof( int ) ); if( array == NULL ){ printf( "Can't get memory for that many values.\n" ); exit( EXIT_FAILURE ); } /* ** 读取这些数值。 */ for( i = 0; i < n_values; i += 1 ){ printf( "? " ); if( scanf( "%d", array + i ) != 1 ){ printf( "Error reading value #%d\n", i ); free( array ); exit( EXIT_FAILURE ); } } /* ** 对这些值排序。 */ qsort( array, n_values, sizeof( int ), compare_integers ); /* ** 打印这些值。 */ for( i = 0; i < n_values; i += 1 ) printf( "%d\n", array[i] ); /* ** 释放内存并退出。 */ free( array ); return EXIT_SUCCESS; }
程序11.2 排序一列整型值 sort.c
用于保存这个列表的内存是动态分配的,这样当编写程序时就不必猜测用户可能希望对多少个值进行排序。可以排序的值的数量仅受分配给这个程序的动态内存数量的限制。但是,当程序对一个小型的列表进行排序时,它实际分配的内存就是实际需要的内存,因此不会造成浪费。