11.5 常见的动态内存错误
在使用动态内存分配的程序中,常常会出现许多错误。这些错误包括对NULL指针进行解引用操作、对分配的内存进行操作时越过边界、释放并非动态分配的内存,试图释放一块动态分配的内存的一部分,以及一块动态内存被释放之后被继续使用。
/*
** 常见的动态内存错误。
*/
#include <stdio.h>
#include <stdlib.h>
int main( void ){
int *p = NULL;
/*
** error 01
** dereference p and assign value to *p;
** in my compiler, dereference p doesn't report error, but assign value to *p reports error.
** *p = 1;
*/
p = (int *)malloc( 3 * sizeof(int) );
/*
** error 02
** access beyond border elements for dynamic memory.
** my compiler is dev-c++, this compiler doesn't report this error.
** p[2] = 3;
** p[5] = 3;
*/
int p2[4];
/*
** error 03
** free memory that is not dynamic.
** this compiler reports warning during compilation, runs mistakenly.
** free( p2 );
*/
/*
** error 04
** free the part of dynamic memory.
** this compiler doesn't report any error, but runs mistakenly.
** free( p + 1 );
*/
free( p );
/*
** error 05
** use memory has been freed.
** this compiler reports error during compilation, runs mistakenly.
** *p = 1;
** printf( "%d\n", *p );
*/
return EXIT_SUCCESS;
}
/* 输出:

*/
警告:
动态内存分配最常见的错误就是忘记检查所请求的内存是否成功分配。程序11.1展现了一种技巧。可以很可靠地进行这个错误检查。MALLOC宏接受元素的数目以及每种元素的类型,计算总共需要的内存字节数,并调用alloc获得内存。alloc调用malloc并进行检查,确保返回的指针不是NULL。
这个方法最后一个难解之处在于第一个非比寻常的#define指令。它用于防止其他代码直接塞入程序而导致的偶尔直接调用malloc的行为。增加这个指令以后,如果程序偶尔直接调用了malloc,程序将由于语法错误而无法编译。在alloc中必须加入#undef指令,这样它才能调用malloc而不至于出错。
警告:
动态内存的第二大错误来源是操作内存是超出了分配内存的边界。
第一种问题显而易见:被访问的内存可能保存了其他变量的值。对它进行修改将破坏那个变量,修改那个变量将破坏存储在那里的值。这种类型的bug非常难以发现。
第二种问题不是那么明显。在malloc和free的有些实现中,它们以链表的形式维护可用的内存池。对分配的内存之外的区域进行访问可能破坏这个链表,这有可能产生异常,从而终止程序。
/*
**定义一个不易发生错误的内存分配器。
*/
#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
/*
** 定义一个不易发生错误的内存分配器。
** alloc.h。
*/
#ifndef ALLOC_H
#define ALLOC_H
#include <stdlib.h>
#define malloc /*不要直接调用malloc*/
#define MALLOC( num, type ) (type *)alloc( (num) * sizeof(type) )
extern void *alloc( size_t size );
#endif
/*
** 不易发生错误的内存分配器的实现。
** alloc.cpp。
*/
#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( EXIT_FAILURE );
}
return new_mem;
}
/*
**一个使用很少引起错误的内存分配器的程序。
** a_client.cpp。
*/
#include "alloc.h"
#include <stdio.h>
int main( void ){
int *new_memory;
int i;
int memory_num;
int counter;
i = 0;
memory_num = 25;
/*
**获得一串整型数的空间。
*/
new_memory = MALLOC( memory_num, int );
/*...*/
counter = 0;
printf( "print elements in dynamic memory:\n" );
while( i < memory_num ){
/*printf( "%d, i = %d\n", new_memory[i], i );*/
printf( "%d ", new_memory[i] );
counter++;
if( counter % 5 == 0 ){
printf( "\n" );
counter = 0;
}
++i;
}
/*
** 释放内存。
*/
free( new_memory );
return EXIT_SUCCESS;
}
/* 输出:

*/
当一个使用动态内存分配的程序失败时,人们很容易把问题的责任推给malloc和free函数。但它们实际上很少是罪魁祸首。事实上,问题几乎总是出在你自己的程序中,而且常常是由于访问了分配内存以外的区域而引起的。
警告:
当使用free时,可能出现各种不同的错误。传递给free的指针必须是一个从malloc、calloc或realloc函数返回的指针。传给free函数一个指针,让它释放一块并非动态分配的内存可能导致程序立即终止或在晚些时候终止。试图释放一块内存的一部分也有可能引起类似的问题,像下面这样:
/*
**Get 10 integers
*/
pi = malloc( 10 * sizeof(int) );
...
/*
**Free only the last 5 integers; keep the first 5
*/
free( pi + 5 );
释放一块内存的一部分是不允许的。动态分配的内存必须整块一起释放。但是,realloc函数可以缩小一块动态分配的内存,有效地释放它尾部的部分内存。
警告:
最后,不要访问已经被free函数释放了的内存。这个警告看上去很显然,但这里仍然存在一个很微妙的问题。假定你对一个指向动态分配的内存的指针进行了复制。而且这个指针的几份副本散布于程序各处。当使用其中一个指针时,你无法保证它所指向的内存是不是已被另一个指针释放。另一方面,必须确保程序中所有使用这块内存被释放之前停止对它的使用。
内存泄漏
当动态内存不再需要时,应该被释放,这样它以后可以被重新分配使用。分配内存但在使用完毕后不释放将引起内存泄漏(memory leak)。在哪些所有执行程序共享一个通用内存池的操作系统中,内存泄漏将一点点地榨干可用内存,最终使其一无所有。要摆脱这个困境,只有重启系统。
其他操作系统能够记住每个程序当前拥有的内存段,这样当一个程序终止时,所有分配给它但未被释放的内存都归返给内存池。但即使在这样的系统中,内存泄漏仍然是一个严重的问题,因为一个持续分配却一点不释放内存的程序最终将耗尽可用的内存。此时,这个有缺陷的程序将无法继续执行下去,它的失败有可能导致当前已经完成的工作统统丢失。