一、二维数组的基本概念
1.1 二维数组的内存映像
- 从内存角度看,二维数组和一维数组一样,在内存中都是连续分布的多个内存单元,并没有本质差别,只是内存的管理方式不一样,如下图所示
- 一维数组
int a[10]
与二维数组int b[2][5]
的对应关系
一维数组 | a[0] | a[1] | a[4] | a[5] | a[9] |
---|---|---|---|---|---|
二维数组 | b[0][0] | b[0][1] | b[0][4] | b[1][0] | b[1][4] |
一个一维数组a[m]
可以表示为二维数组a[x][y](m = x * y)
任一二维数组中的元素a[i][j]
在一维数组中对应的k
为:
k = i * y + j
,即a[i][j] == a[k] == a[i * y + j]
- 二维数组和一维数组在内存使用效率、访问效率上是没有差别的(理想情况下)。在一些特定情况下(比如说矩阵、平面、显示器)用二维数组而不用一维数组,原因在于二维数组好理解、代码好写、利于组织。
1.2 二维数组的访问方式
下标式访问(便于理解):a[i][j]
指针式访问(数组本质):*(*(a + i) + j)
二、二维数组的运算和指针
2.1 指针指向二维数组的数组名
2.1.1 测试代码
#include <stdio.h>
int main(void)
{
int arr[2][5] = {{1, 2, 3, 4, 5},{6, 7, 8, 9, 10}};
int *p1 = NULL;
int **p2 = NULL;
int (*p3)[5] = NULL; //数组指针,指针指向一个数组
p1 = arr;
p2 = arr;
p3 = arr; //a是二维数组的数组名,作为右值表示二维数组第一维的数组首元素首地址,即&a[0]
return 0;
}
2.1.2 测试结果
2.1.3 结果分析
我们可以看出在p1 = arr;
和p2 = arr;
中出现了不兼容的类型装换的警告,这说明int *
和int **
类型都是无法与二维数组数组名进行匹配的,我们来仔细研究一下二维数组的结构:(以int a[m][n]
为例)
m
是第一维,是外层,可以理解为数组a[m]
,里面含有m
个元素,对于任意有意义的元素a[i](i < m - 1)
,它又是含有n
个元素的一维数组n
是第二维,是内层,其本身就是一个数组,数组中存的元素是普通元素,第二维这个数组本身作为元素存储在第一维的数组中
可以参照下图理解(实际上的内存是连续的,这个在上文中说明过)
而对于p3 = arr
,则正好符合二维数组的结构要求:
p3
是一个数组指针,指向的是一个数组- 二维数组的数组名作为右值时表示的是二维数组第一维的数组的首元素首地址,即
&a[0]
下面通过数组指针来访问二维数组,测试结果从略:
for(int i = 0; i < 2; i ++)
{
for(int j = 0; j < 5; j ++)
{
printf("%d ",*(*(p3 + i) + j));
}
}
2.2 指针指向二维数组的第一维完成降维操作
用int *p
来指向二维数组的第一维arr[i]
,先上代码段再分析!
int *p4 = arr[0];
int *p5 = arr[1];
arr[0]
代表二维数组的第一维的第一个元素,相当于是第二维的整体数组的数组名。数组名又表示数组首元素的首地址,因此arr[0]
等同于&arr[0][0]
。
同理,arr[1]
等同于arr[1][0]
那如何访问具体的数呢?其实上面的代码段就已经完成了降维操作,我们现在就可以像一维数组一样访问二维数组了!如下所示:
int a00 = *(p4 + 0);
int a12 = *(p4 + 2);
printf("%d\n%d\n", a00, a12);
测试结果从略。
三、二维数组的动态内存分配及释放
3.1 不知任一维,需从终端读取
测试使用的主函数
#include <stdio.h>
#include <malloc.h>
#define Status int
enum MyStatus
{
ERROR = 0,\
OK = 1,\
FALSE = 0,\
TURE = 1
};
int** ArrMalloc2d(int ***, const int, const int);
Status ArrFree2d(int ***, const int);
int main(void)
{
int m, n;
printf("请输入二维数组的第一维:\n");
scanf("%d", &m);
printf("请输入二维数组的第二维:\n");
scanf("%d", &n);
int **pArr = NULL;
int **arr = NULL;
arr = ArrMalloc2d(&pArr, m, n);
if(arr)
{
printf("内存分配成功!\n");
//测试地址是否正确传出
printf("debug1:in main pArr = %p\n", pArr);
printf("debug2:in main arr = %p\n", arr);
}
int count = 0;
printf("debug:正在赋值\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
*(*(arr + i) + j) = count ++;
}
}
printf("debug:赋值成功\n");
printf("debug:以下为指针式访问:\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%d ",*(*(arr + i) + j));
}
printf("\n");
}
printf("debug:以下为下标式访问:\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
printf("debug:以下为每个元素的地址\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%p ",&(*(*(arr + i) + j)));
}
printf("\n");
}
int flag1 = ArrFree2d(&arr, m);
int flag2 = ArrFree2d(&pArr, m);
if(flag1 && flag2 && ! arr && ! pArr)
{
printf("内存释放成功!\n");
}
else
{
printf("ERROR!\n");
}
return 0;
}
3.1.1 方式一
原理:
- 分配第一维:先分配一块存放
m
个int *
类型元素的内存,将malloc
返回的首地址强制转换为int **
类型,赋给二重指针arr
- 分配第二维:再循环分配
m
个分别存放n
个int
类型元素的内存,将malloc
返回的首地址强制转换为int *
类型赋给一重指针*(arr + i)
函数实现
/**
* @brief 二维数组动态内存分配
* @param arr 输出型参数,输出分配内存的首地址
* @param m, n 输入型参数,二维数组的维度
* @return 分配内存的首地址
*/
int** ArrMalloc2d(int ***arr, const int m, const int n)
{
*arr = (int **)malloc(m * sizeof(int *));
if(! *arr)
{
return ERROR;
}
for (int i = 0; i < m; i ++)
{
*(*arr + i) = (int *)malloc(n * sizeof(int));
if(! *(*arr + i))
{
return ERROR;
}
}
return *arr;
}
/**
* @brief 堆内存释放
* @param arr 待释放的内存首地址
* @param m 维度
* @return 函数运行状态
*/
Status ArrFree2d(int ***arr, const int m)
{
/*先释放内层再释放外层*/
/*内存是循环多次分配的,也需要循环多次释放*/
for (int i = 0; i < m; i ++)
{
free(*(*arr + i));
*(*arr + i) = NULL;
if(*(*arr + i))
{
return ERROR;
}
}
free(*arr);
*arr = NULL;
if(*arr)
{
return ERROR;
}
return OK;
}
测试结果
仔细观察结果,我们会发现:
- 每个数组内部的地址空间是连续的(
&a[i]
连续、在i相同
的情况下&a[i][j]
连续) - 数组间的地址空间没有连续性(观察
&a[i][n - 1]
和&a[i + 1][0]
,可以发现,地址差值并不是sizeof(int)
)。
根据分配原理,其实很容易就可以看出问题:在第一维指向第二维的时候并没有建立第二维地址之间的索引关系,只是孤立地指向一块能存放n
个int
类型元素的内存空间的首地址(第二维)。如下图所示:
优点:分配原理简单易懂且可以利用碎片化的空间,也是受到广泛采纳的
缺点:在对整块内存进行操作时会遇到问题,比如说对所有元素清零:memset(arr, 0, m * n);
是不可行的。使用上述语句原意是把m块m*n个内存单元清零,但是实际上的*(arr + i)
并不是连续的,但是使用memset
时会清除连续m * n
个arr
及其物理连续的内存单元,是及其危险的行为!如下图所示:
3.1.2 方式二
原理:
- 分配第一维:先分配一块存放
m
个int *
类型元素的内存 - 分配第二维:分配一块存储
m * n
个元素数据的内存空间,将首地址返回给** arr
作为二维数组的索引。arr[i]
的内存地址通过arr[i - 1]
进行索引获得
函数实现
int** ArrMalloc2d(int ***arr, const int m, const int n)
{
*arr = (int **)malloc(m * sizeof(int *));
if(! *arr)
{
return ERROR;
}
**arr = (int *)malloc(m * n * sizeof(int));
if(! **arr)
{
return ERROR;
}
for (int i = 1; i < m; i ++)
{
*(*arr + i) = *(*arr + i - 1) + n;
/*如果看不懂这个代码,请复习一下*p + 1是什么意思*/
if(! *(*arr + i))
{
return ERROR;
}
}
return *arr;
}
Status ArrFree2d(int ***arr, const int m)
{
free(*(*arr + 0));
*(*arr + 0) = NULL;
if(*(*arr + 0))
{
return ERROR;
}
free(*arr);
*arr = NULL;
if(*arr)
{
return ERROR;
}
return OK;
}
测试结果
结合函数原理我们不难知道,这样分配的内存空间肯定是连续的,如下图所示。因而对整块内存空间进行操作也是莫得问题滴!比如说memset
3.2 知道第二维
在上述两种方式中,其实都没有严格遵循C语言中二维数组名的本质((*p)[])
,而是根据内存特征构建了一种“基于二重指针的新的管理方式的二维数组”,下面用(*p)[n]
来对内存进行分配。但这只适用于知道二维数组第二维的场合下使用
函数实现
/**
* @brief 已知第二维的动态内存分配
* @param arr 数组指针
* @param m, n各维的数
* @param status 内存分配的状态
* @return 首地址
*/
int** ArrMalloc2d(int (*arr)[], const int m, const int n, int *status)
{
arr = (int (*)[n])malloc(m * sizeof(int *));
if(! arr)
{
*status = ERROR;
}
*status = OK;
return (int **)arr;
}
测试使用的主函数
#include <stdio.h>
#include <malloc.h>
#define Status int
enum MyStatus
{
ERROR = 0,\
OK = 1,\
FALSE = 0,\
TURE = 1
};
int** ArrMalloc2d(int (*)[], const int m, const int n, int *);
int main(void)
{
const int n = 6;
int m;
printf("已知二维数组的第二维是:%d\n", n);
printf("请输入二维数组的第一维:\n");
scanf("%d", &m);
int (*arr)[n] = NULL;
int malloc_status = ERROR;
arr = (int (*)[n])ArrMalloc2d(arr, m, n, &malloc_status);
//arr = (int (*)[n])malloc(m * sizeof(int *));
if(arr && malloc_status)
{
printf("内存分配成功!\n");
printf("%p\n", arr);
}
int count = 0;
printf("debug:正在赋值\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
*(*(arr + i) + j) = count ++;
}
}
printf("debug:赋值成功\n");
printf("debug:以下为指针式访问:\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%-2d ",*(*(arr + i) + j));
}
printf("\n");
}
printf("debug:以下为下标式访问:\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%-2d ", arr[i][j]);
}
printf("\n");
}
printf("debug:以下为每个元素的地址\n");
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j ++)
{
printf("%p ",&(*(*(arr + i) + j)));
}
printf("\n");
}
free(arr);
arr = NULL;
if(!arr)
{
printf("内存释放成功!\n");
}
else
{
printf("ERROR!\n");
}
return 0;
}