动态内存分配
为什么使用动态内存分配
当我们声明一个数组时,必须在编译时确定它的大小,可能有人会异想天开,觉得先让程序读入一个数字,然后再声明,这样的做法是错误的。但在实际工作中,我们会很经常遇到只有在运行时才能确定数组长度的情况。有人会想,可以用一个极长的数组作为存储,那么势必会造成一些空间上的浪费,哪怕用几千几万的单元作为数组的长度,数组仍然可能不够用。好在,C语言为我们提供了动态内存分配
malloc和free
C函数提供了两个函数,malloc和free,分别用于执行动态内存分配和内存释放。这些函数维护一个可用的内存池。当需要一些内存时,调用malloc函数,malloc从内存池中提取出一块合适的内存,并向程序返回一个指向该内存的指针。这块内存并没有以任何的方式进行初始化。如果这块内存需要初始化,要嘛手动,要嘛使用calloc函数。当一块以前分配的内存不再使用时,程序调用free函数把它归还给内存池供以后之需
这两个函数的原型如下:
void *malloc(size_t size); void free(void *pointer);
malloc的参数就是需要分配的内存字节数。如果内存中的可用内存可以满足这个需求,malloc就返回一个指向被分配内存块起始位置的指针。这一块连续的内存,且这块内存可能比我们请求的内存还要多一些,具体由编译器定义。但是,如果内存是空的,或者现有内存无法满足我们的需求,那么malloc就会返回一个NULL指针。因此,对每个malloc返回的指针都要先校验它是否为NULL
free的参数要嘛是NULL,要嘛是先前从malloc、calloc、realloc(莫慌,后面两个函数很快就会介绍到)返回的值。向free传递一个NULL指针不会有任何效果
malloc又是如何知道我们请求的内存是用来存储整型值?又或者是结构体甚至是数组呢?实际上它并不知道,从前面的函数原型上我们可以看到,malloc返回的是一个void指针,正是由于这个缘故,标准表示一个void*类型的指针可以转换为其他类型的指针。但是有些编译器可能会要求你在转换时使用强制类型转换
calloc和realloc
之前我们有看到过calloc函数和realloc。它们的原型如下:
void *calloc(size_t num_elements, size_t element_size); void *realloc(void *ptr, size_t new_size);
calloc也用于内存分配。malloc和calloc之间的主要区别是后者返回指向内存的指针之前把它初始化为0。另外一个需求是calloc和malloc请求内存的方式不同,calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值,它能够计算总共需要多大的内存
realloc函数用于修改一个原先已经分配的内存的大小。使用这个函数,你可以对一块内存扩大或者缩小。如果是扩大的操作,那么原先的内容依然保留,新添加的内存添加到原先内存的后面,新的内存并没有初始化。如果是用于缩小一块内存,该内存尾部的部分内存将被拿掉,剩余部分内存的原先内容依然保留
如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用realloc所返回的新指针
使用动态分配的内存
这里我们测试一下malloc和free这两个函数,我们来写这样一个程序,我们数组一个大于0的数字,然后这个数字就是某个整型数组的长度,我们用malloc申请一块内存,判断malloc返回的指针是否为NULL,如果不为NULL则把该指针作为数组的起始地址,数组的每个元素从0逐个加1,然后打印数组,最后释放之前申请的内存
#include <stdio.h> #include <stdlib.h> int main(int argc, char const *argv[]) { int n, m, i = 0; printf("请输入数组的长度:"); scanf("%d", &n); //<1> int *pi = malloc(n * sizeof(int)); //<2> if (pi == NULL) { printf("Out of memory!\n"); //<3> exit(1); } for (; i < n; i++) { *(pi + i) = i; //<4> } printf("打印数组:"); for (i = 0; i < n; i++) { printf("%d ", *(pi + i)); //<5> } printf("\n"); free(pi); //<6> return 0; }
- 让用户输入数组的长度
- 这里我们用malloc(n * sizeof(int))来申请内存,因为之前说过,malloc的参数为申请内存的字节数,虽然我们要申请n个连续的整型内存,但是单独的n并不能作为malloc的参数,必须是n * sizeof(int)才能表名要申请n个连续的整型内存
- 如果malloc返回的指针为NULL,则打印内存溢出,并退出程序,如果内存申请成功,则进行下一步
- *(pi + i) = i可以让数组中的元素逐个加1,这里我们也可以用pi[i] = i
- 打印数组
- 释放之前申请的内存
我们测试一下代码:
# gcc main.c -o main # ./main 请输入数组的长度:6 打印数组:0 1 2 3 4 5
常见的动态内存错误
使用动态内存分配的程序中,常常会出现许多错误,比如对NULL指针的解引用、对分配的内存进行操作时越过边界、释放非动态分配的内存,试图释放一块动态内存的一部分,以及一块动态内存被释放后仍继续使用。动态内存很常见的错误就是忘记检查是否成功申请了内存,因为malloc函数也有可能返回NULL指针,如果我们很不幸地从malloc函数中拿到一个NULL指针,不检查则对其解引用,那将发生错误。下面,让我们来写一个程序,在对内存进行申请后检查返回的指针是否有效
定义一个不易发生错误的内存分配器
#include <stdlib.h> #define malloc //不直接调用malloc /* *定义MALLOC宏,接受元素的数目和类型,alloc函数调用malloc申请内存,并进行检查返回的指针是否为NULL */ #define MALLOC(num, type) (type *)alloc((num) * sizeof(type)) extern void *alloc(size_t size);
不易发生错误的内存分配器的实现
#include <stdio.h> #include "alloc.h" #undef malloc //alloc中必须加入#undef指令,这样程序才能调用malloc而不至于出错 void *alloc(size_t size) { void *new_mem; new_mem = malloc(size); if (new_mem == NULL) { printf("Out of memoty!\n"); exit(1); } return new_mem; }
使用内存分配器
#include <stdio.h> #include "alloc.h" int main() { int *new_item; int i = 0; new_item = MALLOC(6, int); for (; i < 6; i++) { new_item[i] = i; } printf("打印数组:"); for (i = 0; i < 6; i++) { printf("%d ", new_item[i]); } printf("\n"); return 0; }
运行结果:
# gcc main.c alloc.c -o main # ./main 打印数组:0 1 2 3 4 5
内存泄露:当动态分配的内存不再使用时,它应该被释放,这样它以后可以被重新分配使用。如果不将使用完毕的内存释放将会引起内存泄露,在那些所有执行程序共享一个通用内存池的操作系统中,内存泄露将一点一点榨干可用内存,最终使其一无所有,要摆脱这个困境要嘛终止程序,要嘛重启系统。操作系统一般会记住每个程序当前拥有的内存段,这样当一个程序终止时,所有分配给它但未释放的内存都将归还给内存池,但即便是这样,内存泄露依然是一个十分严重的问题,因为一个持续分配内存却不释放的程序终将耗光所有的可用内存,此时,这个有缺陷的程序将无法继续执行下去,它的失败有可能导致当前已经完成的工作统统丢失
内存分配实例
动态内存分配一个常见的用途是为那些在运行时才能确定长度的数组分配内存空间,如下面的代码,读取一列整数,然后按升序排序,最后打印这个列表
#include <stdio.h> #include <stdlib.h> int compare_integers(void const *a, void const *b) //<1> { register int const *pa = a; register int const *pb = b; return *pa > *pb ? 1 : *pa < *pb ? -1 : 0; } int main(int argc, char const *argv[]) { int *array; int n_values; int i; printf("请输入数组长度:"); if (scanf("%d", &n_values) != 1 || n_values <= 0) //<2> { printf("Illegal number of values\n"); exit(EXIT_FAILURE); } array = malloc(n_values * sizeof(int)); //<3> if (array == NULL) { printf("Out of memory\n"); exit(EXIT_FAILURE); } for (i = 0; i < n_values; i++) { printf("第%d个数字:", i); if (scanf("%d", array + i) != 1) //<4> { printf("在读取第%d个值时出错\n", i); free(array); exit(EXIT_FAILURE); } } qsort(array, n_values, sizeof(int), compare_integers); //<5> printf("打印数组:"); for (i = 0; i < n_values; i++) { printf("%d ", array[i]); //<6> } printf("\n"); free(array); //<7> return EXIT_SUCCESS; }
- 该函数用qsort调用,比较整型值的大小
- 读取数组的长度
- 根据之前读取的长度给数组分配内存
- 读取数组中的值
- 调用qsort函数,对数组进行排序
- 打印数组
- 释放内存
我们测试一下这段代码:
# gcc main.c -o main # ./main 请输入数组长度:6 第0个数字:6 第1个数字:7 第2个数字:1 第3个数字:5 第4个数字:9 第5个数字:2 打印数组:1 2 5 6 7 9
最后一个例子说明了怎样使用动态内存分配来消除使用变体记录造成的内存空间浪费:
/* **包含零件专用信息的结构 */ typedef struct { int cost; int supplier; } Partinfo; /* **存储装配件专用信息的结构 */ typedef struct { int n_parts; struct SUBASSYPART { char partno[10]; short quan; } * part; } Subassyinfo; /* **存货记录结构,它是一个变体 */ typedef struct { char partno[10]; int quan; enum { PART, SUBASSY } type; union { Partinfo *part; Subassyinfo *subassy; } info; } Invrec;
第一个结构保存零件的专用信息,第二个结构保存装配件的专用信息,最后一个声明用于存货记录,它包含了零件和装配件的一些共有信息以及一个变体部分。由于变体部分的不同字段具有不同长度,所以联合包含了指向结构的指针而不是结构本身。动态分配允许程序创建一条存货记录,它所使用的内存的大小就是进行存储的项目的长度,这样就不会浪费内存
下面的代码为每个装配件创建一条存货记录:这个任务取决于装配件所包含的不同零件的数目,所以这个值是作为参数传递给函数的,这个函数为三样东西分配内存,存货记录、装配件结构和装配件结构中的零件数组。如果这些分配中的任何一个失败,所有已经分配的内存将释放,函数返回一个NULL指针,否则,type和info.subassy->n_parts字段被初始化,函数返回一个指向该记录的指针
#include <stdlib.h> #include <stdio.h> #include "inventor.h" /* **用于创建SUBASSEMBLY(装配件)存货记录的函数 */ Invrec *create_subassy_record(int n_parts) { Invrec *new_rec; /* **试图为new_rec分配内存 */ new_rec = malloc(sizeof(Invrec)); if (new_rec != NULL) { /* **内存分配成功,现在存储Subassyinfo部分 */ new_rec->info.subassy = malloc(sizeof(Subassyinfo)); if (new_rec->info.subassy != NULL) { /* **为零件获取一个足够大的数组 */ new_rec->info.subassy->part = malloc(n_parts * sizeof(struct SUBASSYPART)); if (new_rec->info.subassy->part != NULL) { /* **获取内存,填充我们已经知道的字段,然后返回 */ new_rec->type = SUBASSY; new_rec->info.subassy->n_parts = n_parts; return new_rec; } /* *分配内存失败,释放我们原先分配的内存 */ free(new_rec->info.subassy); } free(new_rec); } return NULL; }
最后一个是用于销毁存货记录的函数。这个函数对两种类型的存货记录都适用,它适用一条switch语句判断传递给它的记录的类型并释放所有动态分配给这个记录的内存,最后将这个记录删除
#include <stdlib.h> #include "inventor.h" /* **释放存货记录函数 */ void discard_inventory_record(Invrec *record) { /* **删除记录中的变体部分 */ switch (record->type) { case SUBASSY: free(record->info.subassy->part); free(record->info.subassy); break; case PART: free(record->info.part); break; } //删除主体部分 free(record); }