基本存储类别和动态内存分配
本文部分内容参考了C Primer Plus(sixth edition)一书
存储类别和内存分布
简单介绍一下变量的存储类别和它的内存分布,我们先通过一张表来了解一些基本术语:
术语 | 解释 | 举例 |
作用域 | 描述程序中可访问的标识符的区域 | 块作用域,函数作用域,函数原型作用域,文件作用域 |
链接 | 变量属于哪一部分私有 | 外部链接(全局可见),内部链接(单一文件可见),无链接(块内可见) |
存储期 | 描述通过标识符访问对象的生命期 | 静态存储期,线程存储期,自动存储期,动态分配存储期 |
翻译单元 | 一个源文件包含多个头文件,编译时当作一个翻译单元,作一个文件 | 头文件包含在源代码文件内 |
〉〉块指的是一对花括号括起来的代码。C99之后,块也可以是循环语句+循环体(单一语句)。具有块作用域的变量只能在块内可见。C99之前,变量只允许声明在块的开头。无链接。
〉〉函数作用域仅用于goto标签,一个标签首次出现在函数的内层块中,它的作用域将延伸直整个函数。无链接。
〉〉函数原型作用域:声明函数原型时形参的作用域,仅限于原型。无链接。注意,如果在某一函数int fun的原型中声明变长数组,应该这样声明int fun(int n, int m, int vla[n][m]);
〉〉文件作用域:定义在函数外,从定义处到文件末尾均可见。具有内部和外部链接。
〉〉外部链接:具有外部链接的变量在程序的所有文件中都可见。例如,main.c中定义了一个外部链接变量int var;在lock.c中就要用extern int x;来声明使用。
〉〉内部链接:具有该链接的变量只在该文件可见。例如:static int var; var使用时不必加上像外部链接变量的声明。
〉〉无链接:不具有文件作用域的变量无链接。
〉〉静态存储期:生命(在内存中存在时间):程序开始到结束。
〉〉线程存储期:生命:从线程开始到结束。
〉〉自动存储期:生命以块为参照,离开块即消失。
〉〉动态分配存储期:由程序员管理。
总结表如下:
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
自动 | 自动 | 块 | 无 | 块内 |
静态外部链接 | 静态 | 文件(整个程序) | 外部 | 所有函数外,在另一文件使用前需要用extern再次声明 |
静态内部链接 | 静态 | 文件(单一翻译单元) | 内部 | 所有函数外,用static声明 |
静态无链接 | 静态 | 块 | 无 | 块内,用static声明 |
此外还有寄存器变量,由于这种变量实用性不高,不讨论。
在内存中,静态数据(包括常量)占用一个区域,自动数据占用另一个区域,动态分配的数据占第三个区域(内存堆)。
动态内存分配函数malloc()和释放函数free()
在C99标准发布之前,我们不能使用变量作为声明数组的元素个数。如果我们要创建一个自定义长度的数组,就不能使用原来定义普通静态数组的方法。那么该怎么办呢?这时候就要通过动态分配内存来声明动态数组了。
malloc()函数原型在stdlib.h文件中。它返回一个指向void类型的指针,这个指针指向分配内存的首地址。下面这段代码分配一个具有10个int类型元素的动态数组,用指针ptr指向这个数组的首元素。
int *ptr, n = 10; ptr = (int *)malloc( (size_t)n * sizeof(int) );
malloc()的括号里应填入所需要分配的内存大小,以字节为单位,类型是size_t。为了提供程序的可移植性,我们用了sizeof()运算符来获取当前系统int类型的变量所占用的字节大小。建议大家使用强制类型转换来使用指向void类型的指针。
但是,如果分配内存失败了,会怎么样?malloc()函数会返回一个指向NULL的指针,如果程序在内存分配失败后继续使用内存,就会导致程序异常终止。所以,在分配内存之后,需要检查一下成功分配了没有。
int *ptr, n = 10; ptr = (int *)malloc( (size_t)n * sizeof(int) ); if(ptr == NULL) exit(EXIT_FAILURE);
或者:
int *ptr, n = 10; if( !( ptr = (int *)malloc( (size_t)n * sizeof(int)) ) ) exit(EXIT_FAILURE);
都是很好的方法。
我们知道,malloc()分配的内存具有动态分配存储期,由程序员控制变量的生命期。如果程序结束前没有及时地释放它,程序结束之后就仍然存在。更糟糕的是,如果程序运行中,指向该块内存首地址的指针全部被销毁,那么这块内存就永远无法正确访问,也就无法释放这块内存,这就会导致内存泄露。内存泄露问题很严重,会导致系统停止运行!因为没有空余的内存空间,所以,在分配内存之后如果不需要再次使用,一定要释放!警惕不小心更改指向动态分配内存空间的指针!释放动态分配的内存,我们用free()函数。
free(ptr);
这行代码就释放了之前用malloc()分配的内存空间。
如果ptr所指向的内存块不是通过malloc(),calloc()或realloc()分配的,或者指向一个已经删除的空间,该结果未定义!
动态数组排序实例
动态数组和变长数组(C99)类似,但不表示数组的长度在程序运行中是可变的,只是说可以用变量来表示数组长度。
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <conio.h> 4 5 int main(int argc, char * argv[]){ 6 int * ptr; 7 int key, size, i, j; 8 9 printf("请输入动态数组大小:"); 10 while( !( scanf("%d",&size) ) || size < 0 ){ 11 printf("输入错误,请重新输入。\n:"); 12 while( getchar() != '\n' );//调整输入缓冲区 13 } 14 15 if( !( ptr = (int *)malloc( (size_t)size * sizeof(int)) ) ){//分配内存 16 printf("内存请求失败!"); 17 exit(EXIT_FAILURE); 18 } 19 20 printf("请依次输入数组元素内容:\n"); 21 for(i = 0; i < size; i++) 22 scanf("%d",&ptr[i]); 23 while( getchar() != '\n' ); 24 25 for(i = 1; i < size; i++){//插入排序 26 j = i - 1; 27 key = ptr[i]; 28 while( j >= 0 && ptr[j] > key){ 29 ptr[j + 1] = ptr[j]; 30 j--; 31 } 32 ptr[j + 1] = key; 33 } 34 35 printf("各元素排序后输出如下:"); 36 for(i = 0; i < size; i++) 37 printf("%d ",ptr[i]); 38 39 free(ptr); //释放内存,重中之重 40 41 _getch(); 42 return 0; 43 }
运行结果:
动态内存分配calloc()函数
分配内存,除了使用malloc()函数外,还可以使用calloc()函数。calloc()函数和malloc()函数最大的区别就是:calloc()函数在分配内存之后,会把块中的所有位置0(某些硬件系统中,不是把所有的位都置0来表示浮点值0)。
calloc()函数接受两个size_t类型的参数。第一个参数表明存储单元的数量,第二个参数表示每个存储单元的大小(以字节为单位)。
现在如果我们要申请一个元素个数为n个的int类型动态数组ar,我们可以这样做:
int * ar; int n = 10; ar = (int *)calloc((size_t)n, sizeof(int) ); if( ar == NULL ) exit(EXIT_FAILURE);
如果分配内存成功,此时,ar数组里的每一个元素都为0。
同样,我们可以使用free()来释放calloc()分配的内存:
free(ar);
补充:realloc()函数
原型:void *realloc(void * ptr, size_t size);
简单描述:把ptr指向的内存空间更改为size字节,size字节内的内存块内容不变。该函数返回块的位置。如果不能重新分配空间,则返回NULL。如果ptr为NULL,其行为与调用带size参数的malloc()相同。如果size为0且ptr不是NULL,其行为与调用带ptr参数的free相同。
不推荐大量使用这个函数,因为当不能重新分配空间时,函数返回NULL,原来已分配的内存块地址丢失,会造成内存泄露。尽管在很多时候这个函数非常方便。如果你需要使用这个函数,请先保存原来内存块的地址!