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

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

变量有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() 释放掉,造成了内存泄露。

 

转载于:https://www.cnblogs.com/chunlanse2014/articles/4421942.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值