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

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

变量有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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言教程 C语言概述 7 C语言的发展过程 7 当代最优秀的程序设计语言 7 C语言版本 7 C语言的特点 7 面向对象的程序设计语言 8 C和C++ 8 简单的C程序介绍 8 输入和输出函数 9 C源程序的结构特点 10 书写程序时应遵循的规则 10 C语言的字符集 11 C语言词汇 11 Turbo C 2.0集成开发环境的使用 12 Turbo C 2.0简介和启动 12 Turbo C 2.0集成开发环境 13 File菜单 13 Edit菜单 14 Run菜单 15  Compile菜单 16 Project菜单 18 Options菜单 19 Debug菜单 23 Break/watch菜单 24 Turbo C 2.0的配置文件 24 程序的灵魂—算法 26 算法的概念 26 简单算法举例 26 算法的特性 29 怎样表示一个算法 29 用自然语言表示算法 29 用流程图表示算法 29 三种基本结构和改进的流程图 33 用N-S流程图表示算法 34 用伪代码表示算法 35 用计算机语言表示算法 35 结构化程序设计方法 36 数据类型、运算符与表达式 37 C语言的数据类型 37 常量与变量 39 常量和符号常量 39 变量 40 整型数据 40 整型常量的表示方法 40 整型变量 41 实型数据 44 实型常量的表示方法 44 实型变量 45 实型常数的类型 46 字符型数据 46 字符常量 46 转义字符 46 字符变量 47 字符数据在内存中的存储形式及使用方法 47 字符串常量 48 变量赋初值 48 各类数值型数据之间的混合运算 49 算术运算符和算术表达式 51 C运算符简介 51 算术运算符和算术表达式 51 赋值运算符和赋值表达式 53 逗号运算符和逗号表达式 55 小结 55 C的数据类型 55 基本类型的分类及特点 55 常量后缀 56 常量类型 56 数据类型转换 56 运算符优先级和结合性 56 表达式 56 最简单的C程序设计—顺序程序设计 57 C语句概述 57 赋值语句 58 数据输入输出的概念及在C语言中的实现 59 字符数据的输入输出 60 putchar 函数(字符输出函数) 60 getchar函数(键盘输入函数) 60 格式输入与输出 61 printf函数(格式输出函数) 61 scanf函数(格式输入函数) 63 顺序结构程序设计举例 67 分支结构程序 69 关系运算符和表达式 69 关系运算符及其优先次序 69 关系表达式 69 逻辑运算符和表达式 70 逻辑运算符极其优先次序 70 逻辑运算的值 70 逻辑表达式 71 if语句 71 if语句的三种形式 71 if语句的嵌套 75 条件运算符和条件表达式 77 switch语句 77 程序举例 79 循环控制 81 概述 81 goto语句以及用goto语句构成循环 81 while语句 81 do-while语句 83 for语句 86 循环的嵌套 88 几种循环的比较 88 break和continue语句 88 break语句 88 continue 语句 89 程序举例 90 数组 94 一维数组的定义和引用 94 一维数组的定义方式 94 一维数组元素的引用 95 一维数组的初始化 96 一维数组程序举例 97 二维数组的定义和引用 98 二维数组的定义 98 二维数组元素的引用 98 二维数组的初始化 99 二维数组程序举例 101 字符数组 101 字符数组的定义 101 字符数组的初始化 101 字符数组的引用 101 字符串和字符串结束标志 102 字符数组的输入输出 102 字符串处理函数 104 程序举例 106 本章小结 109 函 数 109 概述 110 函数定义的一般形式 111 函数的参数和函数的值 112 形式参数和实际参数 113 函数的返回值 114 函数的调用 114 函数调用的一般形式 114 函数调用的方式 115 被调用函数的声明和函数原型 115 函数的嵌套调用 116 函数的递归调用 118 数组作为函数参数 121 局部变量和全局变量 125 局部变量 125 全局变量 127 变量存储类别 128 动态存储方式与静态动态存储方式 128 auto变量 129 用static声明局部变量 129 register变量 130 用extern声明外部变量 131 预处理命令 131 概述 132 宏定义 132 无参宏定义 132 带参宏定义 135 文件包含 138 条件编译 139 本章小结 141 指针 141 地址指针的基本概念 142 变量的指针和指向变量的指针变量 142 定义一个指针变量 143 指针变量的引用 143 指针变量作为函数参数 147 指针变量几个问题的进一步说明 150 数组指针和指向数组的指针变量 153 指向数组元素的指针 153 通过指针引用数组元素 154 数组名作函数参数 156 指向多维数组的指针和指针变量 162 字符串的指针指向字符串的针指变量 165 字符串的表示形式 165 使用字符串指针变量与字符数组的区别 168 函数指针变量 169 指针型函数 170 指针数组和指向指针的指针 171 指针数组的概念 171 指向指针的指针 174 main函数的参数 176 有关指针的数据类型和指针运算的小结 177 有关指针的数据类型的小结 177 指针运算的小结 177 void指针类型 178 共用体 178 定义一个结构的一般形式 179 结构类型变量的说明 179 结构变量成员的表示方法 181 结构变量的赋值 181 结构变量的初始化 182 结构数组的定义 182 结构指针变量的说明和使用 185 指向结构变量的指针 185 指向结构数组的指针 186 结构指针变量作函数参数 187 动态存储分配 188 链表的概念 189 枚举类型 191 枚举类型的定义和枚举变量的说明 191 枚举类型变量的赋值和使用 192 类型定义符typedef 193 位运算 195 位运算符C语言提供了六种位运算符: 195 按位与运算 195 按位或运算 195 按位异或运算 196 求反运算 196 左移运算 196 右移运算 196 位域(位段) 197 本章小结 199 文件 199 C文件概述 200 文件指针 200 文件的打开与关闭 201 文件的打开(fopen函数) 201 文件关闭函数(fclose函数) 202 文件的读写 202 字符读写函数fgetc和fputc 202 字符串读写函数fgets和fputs 206 数据块读写函数fread和fwtrite 207 格式化读写函数fscanf和fprintf 209 文件的随机读写 210 文件定位 210 文件的随机读写 211 文件检测函数 211 文件结束检测函数feof函数 212 读写文件出错检测函数 212 文件出错标志和文件结束标志置0函数 212 C库文件 212 本章小结 213

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值