动态数组
前面学习的数组,无论是普通数组还是变长数组都是在栈上分配的内存,数组一旦定义就不再由程序员掌控,为其分配内存、回收内存都由系统决定。这些数组需要的内存空间,包括大小与数据类型,在编译时期就已经被确定,在程序执行期间不可改变。但是有时候仅依靠系统帮我们申请的内存空间并不能满足需求,程序在执行过程中可能临时需要一块内存存储数据并完成数据处理,这个时候就需要程序员自己去堆上申请内存空间。
前面章节讲解了堆内存空间操作函数,其中malloc()、calloc()函数、realloc()函数用于向堆内存申请空间,它们申请的内存空间是一段指定数据类型和大小的连续空间,且可以在程序执行时随时使用随时申请,这些函数在程序执行过程中申请的连续空间被称为动态数组。相应的在栈上的数组被称为静态数组。
相比于静态数组,动态数组可以根据用户需要,有效利用存储空间,但其定义方式复杂,且内存申请后需要程序员手动释放,否则会造成内存泄露。
下面通过一个案例演示动态数组的使用,如例1所示。
例1 dynamicarray1.c
1 #include <stdio.h>
2 int main()
3 {
4 int arr_len = 10; //指定生成的数组的长度
5 int * parr; //定义int类型的指针
6 // 申请内存空间作为整型数组,空间大小是sizeof(int) * arr_len个字节
7 parr = (int *)malloc(sizeof(int) * arr_len);
8 memset(parr, 0, sizeof(int) * arr_len);// 将这块内存空间全部初始化为0
9 for (int i = 0; i < arr_len; i++) // 为整型数组的元素赋值
10 {
11 parr[i] = i;
12 }
13 for (int i = 0; i < arr_len; i++) // 打印整型数组的元素的值
14 {
15 printf("%d ", parr[i]);
16 }
17 printf("\n");
18 free(parr); // 释放内存空间
19 return 0;
20 }
例1运行结果如图1所示。
图1 例1运行结果
在例1中,第7行代码调用malloc()函数向堆空间申请了一个40字节(10个4字节)的空间,malloc()返回一个void类型的指针,将该指针强制转换为int类型后赋值给int*类型指针parr。第8行代码调用memset()函数将动态数组parr全部元素初始化为0。第9~12行代码使用for循环为动态数组parr赋值。第13~16行代码使用for循环打印出数组中的元素。第18行代码调用free()函数释放动态数组空间。由图1可知,程序成功输出了动态数组中的数据。
通过内存申请函数也可以创建动态二维数组,在创建动态二维数组的时候,要牢记一个原则是创建动态二维数组时,从外层到里层,逐层创建;释放的时候,从里层到外层,逐层释放内存空间。假如通过malloc()函数创建动态二维数组,首先调用malloc()函数申请一段堆内存空间,每个数据单元块中都存储一个指针,然后再调用malloc()函数为每个指针申请一块堆内存空间,即让指针指向一块堆内存空间,该动态二维数组逻辑示意图可用图2表示。
图2 动态二维数组arr
在图2中,parr是一个二维数组,在创建动态二维数组parr时,先创建存储指针的一维数组,即为parr[0]~parr[5]元素申请一段连续内存空间;再创建每个指针指向的一维数组,即为每一行申请连续内存空间。释放内存时,先释放指针parr[0]~parr[5]指向的内存空间,再释放指针parr指向的指针数组的内存空间。如果先释放指针parr指向的空间,则parr[0]~parr[5]指向的空间没有了指针标识,就无法释放,造成内存泄露。
动态二维数组的创建与使用如例2所示。
例2 dynamicArray2.c
1 #include <stdio.h>
2 Include <stdlib.h>
3 int main()
4 {
5 int n1 = 6, n2 = 4; //定义两个变量
6 int** parr; //定义一个二级指针
7 //申请一段堆内存空间,存储int*类型的指针
8 parr = (int**)malloc(sizeof(int*) * n1);
9 //使用for循环为每个指针分配一段内存空间
10 for (int i = 0; i < n1; i++)
11 parr[i] = (int*)malloc(sizeof(int) * n2);
12 //使用for循环初始化动态二维数组
13 for (int i = 0; i < n1; i++)
14 {
15 for (int j = 0; j < n2; j++)
16 {
17 parr[i][j] = i; //为动态二维数组赋值
18 printf("%3d", parr[i][j]); //输出元素
19 }
20 printf("\n");
21 }
22 //循环释放指针指向的内存空间
23 for (int i = 0; i < n1; i++)
24 free(parr[i]);
25 free(parr); //释放parr指针的空间
26 return 0;
27 }
例2运行结果如图3所示。
图3 例2运行结果
在例2中,第4行代码定义了两个变量n1和n2,分别用于标识动态二维数组的行和列。第5~7行代码定义了一个二级指针parr,调用malloc()函数申请一块堆内存空间,该空间用于存储n1个int*类型的指针数组的首地址。第9~10行代码,通过for循环调用malloc()函数为指针数组的每一个元素申请堆一块堆内存空间,这块堆内存空间是元素个数为n2的一维数组;第12~20行代码使用for循环嵌套为动态二维数组赋值并输出元素值。第22~23行代码释放parr[0]~parr[n1-1]指向的空间,即指向二维数组每行所占的内存空间。第24行代码释放parr指向的空间。由图3可知,程序成功创建了一个动态二维数组,并成功赋值输出。
需要注意的是,动态二维数组与静态二维数组的内存存储方式并不相同,静态二维数组在栈上是线性存储,但动态二维数组在内存中并不是线性存储的,其每一行是连续的,但行与行之间是不连续的。例6-8中动态二维数组内存空间分布可用图4描述。
图4 动态二维数组内存分布