chunlanse2014

导航

动态内存分配及变量存储类别(第一部分)

动态内存分配也就是在程序运行中实时申请内存分配。这有利于我们对任意多的数据进行处理。如果这些数据不用了,我们也可以随时释放。

变量有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()函数传递的值。

 

C语言内存模型示意图
图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个整数:

  1. 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() 函数是一个良好的编程习惯。这不但体现在处理大型程序时的必要性,并能在一定程度上体现程序优美的风格和健壮性。

但是有些时候,常常会有将内存丢失的情况,例如:

  1. int *pOld = (int*) malloc( sizeof(int) );
  2. int *pNew = (int*) malloc( sizeof(int) );


这两段代码分别创建了一块内存,并且将内存的地址传给了指针 pOld 和 pNew。此时指针 pOld 和 pNew 分别指向两块内存。

如果接下来进行这样的操作:

  1. pOld=pNew;

pOld 指针就指向了 pNew 指向的内存地址,这时候再进行释放内存操作:

  1. free(pOld);

此时释放的 pOld 所指向的内存空间就是原来 pNew 指向的,于是这块空间被释放掉了。但是 pOld 原来指向的那块内存空间还没有被释放,不过因为没有指针指向这块内存,所以这块内存就造成了丢失。

另外,你不应该进行类似下面这样的操作:

  1. malloc( sizeof(int) );

这样的操作没有意义,因为没有指针指向分配的内存,无法使用,而且无法通过 free() 释放掉,造成了内存泄露。

 

posted on 2015-04-13 13:24  chunlanse2014  阅读(859)  评论(0编辑  收藏  举报