动态内存分配也就是在程序运行中实时申请内存分配。这有利于我们对任意多的数据进行处理。如果这些数据不用了,我们也可以随时释放。
变量有4种存储类别:auto(自动)、register(寄存器)、static(静态)和extern(外部)。
1. C语言动态内存分配的概念
前面的代码中,不管我们定义变量、函数,还是创建数组,它们需要的内存是固定的,编译器已经分配好了,程序运行时不能改变,我们也不能自由掌控。
所谓动态内存分配(Dynamic Memory Allocation),就是指在程序运行的过程中动态地分配或者释放内存空间。这样能够更加高效的使用内存,需要内存时就立即分配,而且需要多少由程序员决定,不浪费内存空间;不需要时立即回收,再分配给其他程序使用。
在C语言中,只运行使用系统分配的内存,如果系统没有为变量分配内存,那么会出现什么情况呢?请看下面的代码:
1 #include <stdio.h> 2 #include <string.h> 3 int main() 4 { 5 char *p; //字符指针 6 strcpy(p, "cyuyan"); 7 return 0; 8 }
这段代码运行时会报错。因为 “char *p;”语句并没有使指针变量 p 初始化,它指向的地址是任意的(一般是非法的,当前程序没有权限使用);“strcpy(p, "cyuyan");”语句会将字符串复制到没有初始化的指针变量指定的地址中,系统分配内存失败。
但是,字符数组和字符指针不同,在字符数组被声明定义时,系统已经为其分配相应的内存空间,我们就可以使用其存放一定的数据了,下面的代码是正确的:
1 #include <stdio.h> 2 #include <string.h> 3 int main() 4 { 5 char p[20]; 6 strcpy(p, "cyuyan"); 7 return 0; 8 }
到现在为止,我们可以很清楚地知道数组可以保存多个相同类型的数据,但是它有许多缺点,主要表现在两方面:
缺 点 | 说 明 |
---|---|
数组的大小是固定的 | 它所占的空间在内存分配之后的运行期间是不能改变的,所以这就要求我们事先为其分配较大的空间,保证程序运行时不会溢出。 |
数组需要一块连续的内存空间 | 如果对于一个系的各班定义一个数组,每个班的学生人数不一定相同,那么就很难定义数组的长度。过大会造成资源的浪费,过小又会造成溢出,影响程序的运行。 |
为了解决所遇到的内存分配问题,我们使用动态内存分配,根据每次运行程序时要处理的数据的多少随时申请内存,如下图所示:
2. C语言内存模型(内存组织方式)
我们知道,C程序开发并编译完成后,要载入内存(主存或内存条)才能运行,变量名、函数名都会对应内存中的一块区域。
内存中运行着很多程序,我们的程序只占用一部分空间,这部分空间又可以细分为以下的区域:
内存分区 | 说明 |
---|---|
程序代码区(code area) | 存放函数体的二进制代码 |
静态数据区(data area) | 也称全局数据区,包含的数据类型比较多,如全局变量、静态变量、一般常量、字符串常量。其中:
注意:静态数据区的内存在程序结束后由操作系统释放。 |
堆区(heap area) | 一般由程序员分配和释放,若程序员不释放,程序运行结束时由操作系统回收。malloc()、calloc()、free() 等函数操作的就是这块内存,这也是本章要讲解的重点。 注意:这里所说的堆区与数据结构中的堆不是一个概念,堆区的分配方式倒是类似于链表。 |
栈区(stack area) | 由系统自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。 |
命令行参数区 | 存放命令行参数和环境变量的值,如通过main()函数传递的值。 |
图1:C语言内存模型示意图
提示:关于局部的字符串常量是存放在全局的常量区还是栈区,不同的编译器有不同的实现,VC 将局部常量像局部变量一样对待,存储于栈(⑥区)中,TC则存储在静态数据区的常量区(②区)。
注意:未初始化的全局变量的默认值是 0,而未初始化的局部变量的值却是垃圾值(任意值)。请看下面的代码:
1 #include <stdio.h> 2 #include <conio.h> 3 int global; 4 int main() 5 { 6 int local; 7 printf("global = %d\n", global); 8 printf("local = %d\n", local); 9 getch(); 10 return 0; 11 }
运行结果:
global = 0
local = 1912227604
为了更好的理解内存模型,请大家看下面一段代码:
1 #include<stdio.h> 2 #include<stdlib.h> 3 #include<string.h> 4 int a = 0; // 全局初始化区(④区) 5 char *p1; // 全局未初始化区(③区) 6 int main() 7 { 8 int b; // 栈区 9 char s[] = "abc"; // 栈区 10 char *p2; // 栈区 11 char *p3 = "123456"; // 123456\0 在常量区(②),p3在栈上,体会与 char s[]="abc"; 的不同 12 static int c = 0; // 全局初始化区 13 p1 = (char *)malloc(10); // 堆区 14 p2 = (char *)malloc(20); // 堆区 15 // 123456\0 放在常量区,但编译器可能会将它与p3所指向的"123456"优化成一个地方 16 strcpy(p1, "123456"); 17 }
3. C语言动态内存空间的分配
上节我们讲解了C语言的内存模型,了解到堆区的内存空间是由程序员来分配和释放的,称为自由区,其他区域一般不能由程序员随意操作。本节要讲解的动态内存分配就是在堆区进行的。
动态内存分配和释放常用到的四个函数为:malloc()、calloc()、realloc() 和 free()。
这几个函数的具体用法在C标准库中已经进行了讲解(点击上面链接查看),这里不再赘述,仅作简单的对比,并给出一个综合示例。
1) malloc()
原型:void* malloc (size_t size);
作用:在堆区分配 size 字节的内存空间。
返回值:成功返回分配的内存地址,失败则返回NULL。
注意:分配内存在动态存储区(堆区),手动分配,手动释放,申请时空间可能有也可能没有,需要自行判断,由于返回的是void*,建议手动强制类型转换。
2) calloc()
原型:void* calloc(size_t n, size_t size);
功能:在堆区分配 n*size 字节的连续空间。
返回值:成功返回分配的内存地址,失败则返回NULL。
注意:calloc() 函数是对 malloc() 函数的简单封装,参数不同,使用时务必小心,第一参数是第二参数的单元个数,第二参数是单位的字节数。
3) realloc()
原型:void* realloc(void *ptr, size_t size);
功能:对 ptr 指向的内存重新分配 size 大小的空间,size 可比原来的大或者小,还可以不变(如果你无聊的话)。
返回值:成功返回更改后的内存地址,失败则返回NULL。
4) free()
原型:void free(void* ptr);
功能:释放由 malloc()、calloc()、realloc() 申请的内存空间。
注意:每个内存分配函数必须有相应的 free 函数,释放后不能再次使用被释放的内存,建议在 free 函数后把被释放指针置为 NULL,好处有二:
- 再次访问该指针将出错,避免野指针;
- 再次释放该指针不会让程序崩溃只是free函数失效。
对比与说明
在利用 calloc() 函数时,如果对分配的存储空间不保存,那么丢失后就无法找回来,更严重的是这段空间不能再重新分配,因而造成内存的浪费。因此我们较少使用calloc(),推荐使用malloc()。
另外,在分配内存时最好不要直接用数字指定内存空间的大小,这样不利于程序的移植。因为在不同的操作系统中,同一数据类型的长度可能不一样。为了解决这个问题,C语言提供了一个判断数据类型长度的操作符,就是 sizeof。
sizeof 是一个单目操作符,不是函数,用以获取数据类型的长度时必须加括号,例如 sizeof(int)、sizeof(char) 等。
下面的例子演示了如何用 malloc() 和 sizeof 分配内存空间来保存20个整数:
- int *numbers = (int*) malloc( 20 * sizeof(int) );
这种分配内存的方式在数据结构中很常见。
最后是一个综合的示例:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #define N (5) 4 #define N1 (7) 5 #define N2 (3) 6 int main() 7 { 8 int *ip; 9 int *large_ip; 10 int *small_ip; 11 if((ip = (int*)malloc(N * sizeof(int))) == NULL) 12 { 13 printf("memory allocated failed!\n"); 14 exit(1); 15 } 16 int i; 17 for(i = 0; i < N; i++) 18 { 19 ip[i] = i; 20 printf("ip[%d] = %d\t", i, ip[i]); 21 } 22 printf("\n"); 23 if((large_ip = (int* )realloc(ip, N1 * sizeof(int))) == NULL) 24 { 25 printf("memory allocated failed!\n"); 26 exit(1); 27 } 28 for(i = N; i < N1; i++) 29 large_ip[i] = 9; 30 for(i = 0; i < N1; i++) 31 printf("large_ip[%d] = %d\t", i, large_ip[i]); 32 printf("\n"); 33 if((small_ip = (int*)realloc(large_ip, N2 * sizeof(int))) == NULL) 34 { 35 printf("memory allocated failed!\n"); 36 exit(1); 37 } 38 for(i = 0; i < N2; i++) 39 printf("small_ip[%d] = %d\t", i, small_ip[i]); 40 printf("\n"); 41 free(small_ip); 42 small_ip = NULL; 43 system("pause"); 44 return 0; 45 }
运行结果:
代码说明:
1) 代码看似很长,其实较为简单,首先分配一个包含5个整型的内存区域,分别赋值0到4;再用realloc函数扩大内存区域以容纳7个整型数,对额外的两个整数赋值为9;最后再用realloc函数缩小内存区域,直接输出结果(因为realloc函数会自动复制数据)。
2) 这次把分配函数与验证返回值验证写在了一起,为的是书写方便,考虑到优先级问题添加了适当的括号,这种写法较为常用,注意学习使用。
3) 本例free函数只用释放small_ip指针即可,如函数介绍中注意里提到的,另外两个指针已被系统回收,不能再次使用。
4. C语言内存泄露(内存丢失)
使用 malloc()、calloc()、realloc() 动态分配的内存,如果在使用完毕后未释放,就会导致该内存一直被占用,直到程序结束(其实说白了就是该内存空间使用完毕之后未回收),这就是所谓的“内存泄漏”。
内存泄漏形象的比喻是“操作系统可提供给所有进程的内存空间正在被某个程序榨干”,最终结果是程序运行时间越长,占用内存空间越来越多,最终用尽全部内存空间,整个系统崩溃。所以内存泄漏是从操作系统的角度来看的。
另外,动态分配的一块内存如果没有任何一个指针指向它,那么这块内存就泄漏了。
free() 函数的用处在于实时地回收内存,如果程序很简单,程序结束之前也不会使用过多的内存,不会降低系统的性能,那么也可以不用写 free() 函数。当程序结束后,操作系统会释放内存。
但是如果在开发大型程序时不写 free() 函数,后果是很严重的。这是因为很可能在程序中要重复一万次分配10MB的内存,如果每次进行分配内存后都使用 free() 函数去释放用完的内存空间, 那么这个程序只需要使用10MB内存就可以运行。但是如果不使用 free() 函数,那么程序就要使用100GB 的内存!这其中包括绝大部分的虚拟内存,而由于虚拟内存的操作需要读写磁盘,因此,这样会极大地影响到系统的性能,系统因此可能崩溃。
因此,在程序中使用 malloc() 分配内存时都对应地写出一个 free() 函数是一个良好的编程习惯。这不但体现在处理大型程序时的必要性,并能在一定程度上体现程序优美的风格和健壮性。
但是有些时候,常常会有将内存丢失的情况,例如:
- int *pOld = (int*) malloc( sizeof(int) );
- int *pNew = (int*) malloc( sizeof(int) );
这两段代码分别创建了一块内存,并且将内存的地址传给了指针 pOld 和 pNew。此时指针 pOld 和 pNew 分别指向两块内存。
如果接下来进行这样的操作:
- pOld=pNew;
pOld 指针就指向了 pNew 指向的内存地址,这时候再进行释放内存操作:
- free(pOld);
此时释放的 pOld 所指向的内存空间就是原来 pNew 指向的,于是这块空间被释放掉了。但是 pOld 原来指向的那块内存空间还没有被释放,不过因为没有指针指向这块内存,所以这块内存就造成了丢失。
另外,你不应该进行类似下面这样的操作:
- malloc( sizeof(int) );
这样的操作没有意义,因为没有指针指向分配的内存,无法使用,而且无法通过 free() 释放掉,造成了内存泄露。