动态数组、二维数组、指针
本文会讲:
- 二维数组的存储形式
- 数组类型为什么会退化?
- 二维数组的动态分配
1 二维数组的存储形式
以下这个程序可以很好的说明二维数组的存储形式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| #include <stdio.h>
int main(void) { int a[4][2];
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) a[i][j] = i * 10 + j;
printf("sizeof(int):\t%llu\n", sizeof(int)); printf("sizeof(a):\t%llu\n", sizeof(a)); printf("sizeof(a[0]):\t%llu\n", sizeof(a[0]));
printf("\n");
printf("a:\t%p\n", a); printf("&a:\t%p\n", &a); printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("a[%d][%d]:\t%02d\n", i, j, a[i][j]);
printf("\n");
for (int i = 0; i < 4; i++) printf("a[%d]:\t%p\n", i, a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("*a[%d]:\t%d\n", i, *a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("&a[%d]:\t%p\n", i, &a[i]);
printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("&a[%d][%d]:\t%p\n", i, j, &a[i][j]); return 0; }
|
这个程序的运行结果是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| sizeof(int): 4 sizeof(a): 32 sizeof(a[0]): 8
a: 000000000064FDD0 &a: 000000000064FDD0
a[0][0]: 00 a[0][1]: 01 a[1][0]: 10 a[1][1]: 11 a[2][0]: 20 a[2][1]: 21 a[3][0]: 30 a[3][1]: 31
a[0]: 000000000064FDD0 a[1]: 000000000064FDD8 a[2]: 000000000064FDE0 a[3]: 000000000064FDE8
*a[0]: 0 *a[1]: 10 *a[2]: 20 *a[3]: 30
&a[0]: 000000000064FDD0 &a[1]: 000000000064FDD8 &a[2]: 000000000064FDE0 &a[3]: 000000000064FDE8
&a[0][0]: 000000000064FDD0 &a[0][1]: 000000000064FDD4 &a[1][0]: 000000000064FDD8 &a[1][1]: 000000000064FDDC &a[2][0]: 000000000064FDE0 &a[2][1]: 000000000064FDE4 &a[3][0]: 000000000064FDE8 &a[3][1]: 000000000064FDEC
|
可以看出,二维数组是连续储存的,有&a = a = &a[0] = a[0] = &a[0][0]
,以此类推。
我们可以这样表示二维数组
a |
|
|
|
|
|
|
|
a[0] |
|
a[1] |
|
a[2] |
|
a[3] |
|
a[0] [0] |
a[0] [1] |
a[1] [0] |
a[1] [1] |
a[2] [0] |
a[2] [1] |
a[3] [0] |
a[3] [1] |
2 数组类型为什么会退化?
有的时候会出现这种情况:
在主函数里声明了一个二维数组,使用sizeof 会得到它的大小,但传到函数里,却只能得到指针的大小,这是为什么呢?
详见:https://www.zhihu.com/question/464844221/answer/1940453834
3 动态分配二维数组
3.1 第一种方法
先申请一维数组,在将该一维数组强制转换成二维数组
前置知识:
一维数组的数组名是:一个指向数据类型的指针,如int *,初次之外没有任何多余的信息
二维数组的数组名是:一个【指向一维数组的指针】,在这里,一维数组就相当于数据类型,
即int q[][4]
和int (*q)[4]
是等价的,两者都是指向一维数组的指针,当对这个指针 + 2时,就等价于q += 2 * 4 * sizeof(int)
直接贴代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #include <stdio.h> #include <stdlib.h>
int main(void) { int *a = (int *)malloc(2 * 4 * sizeof(int)); for (int i = 0; i < 8; i++) a[i] = i; int (*b)[4] = (int (*)[4])a; for (int i = 0; i < 2; i++) { for (int j = 0; j < 4; j++) printf("%d ", b[i][j]); printf("\n"); } return 0; }
|
我们首先创建了一个一维数组,这个数组存储了8个连续的整数类型。
我们用int (*b)[4]
声明了一个指向 四个元素的数组 的指针。
然后对数组a进行强制类型转换,将它转换成了一个指向4个元素数组的指针。
这样动态分配的指针和直接int b[2][4]
是一样的。
下面我们使用 1 中的方法对这个数组进行仔细检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <stdio.h> #include <stdlib.h>
int main(void) { int *b = (int *)malloc(2 * 4 * sizeof(int)); for (int i = 0; i < 8; i++) b[i] = i; int (*a)[2] = (int (*)[2])b;
printf("sizeof(int):\t%llu\n", sizeof(int)); printf("sizeof(a):\t%llu\n", sizeof(a)); printf("sizeof(a[0]):\t%llu\n", sizeof(a[0]));
printf("\n");
printf("a:\t%p\n", a); printf("&a:\t%p\n", &a); printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("a[%d][%d]:\t%02d\n", i, j, a[i][j]);
printf("\n");
for (int i = 0; i < 4; i++) printf("a[%d]:\t%p\n", i, a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("*a[%d]:\t%d\n", i, *a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("&a[%d]:\t%p\n", i, &a[i]);
printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("&a[%d][%d]:\t%p\n", i, j, &a[i][j]); }
|
运行结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| sizeof(int): 4 sizeof(a): 8 sizeof(a[0]): 8
a: 00000000009C1420 &a: 000000000064FDF0
a[0][0]: 00 a[0][1]: 01 a[1][0]: 02 a[1][1]: 03 a[2][0]: 04 a[2][1]: 05 a[3][0]: 06 a[3][1]: 07
a[0]: 00000000009C1420 a[1]: 00000000009C1428 a[2]: 00000000009C1430 a[3]: 00000000009C1438
*a[0]: 0 *a[1]: 2 *a[2]: 4 *a[3]: 6
&a[0]: 00000000009C1420 &a[1]: 00000000009C1428 &a[2]: 00000000009C1430 &a[3]: 00000000009C1438
&a[0][0]: 00000000009C1420 &a[0][1]: 00000000009C1424 &a[1][0]: 00000000009C1428 &a[1][1]: 00000000009C142C &a[2][0]: 00000000009C1430 &a[2][1]: 00000000009C1434 &a[3][0]: 00000000009C1438 &a[3][1]: 00000000009C143C
|
可以看到除了小部分细节(sizeof(a) 、&a)与我们直接声明二维数组表现得不一样,其他的行为与直接声明二维数组是一模一样的。
这种方法得到的二维数组虽然和原生的二维数组一样,但它也有缺点,就是它不能动态分配行的长度,即列的大小。因此只能说是半个动态数组。而且,更令人烦恼的是,指向数组的指针,int (*)[len] 这个类型也无法作为函数的返回值,或者使用typedef来取别名,不方便我们对它进行包装。
3.2 第二种方法:
参考: https://blog.csdn.net/morewindows/article/details/7664479# 我这里将原文的int 替换成了size_t
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| #include <stdio.h> #include <stdlib.h> #include <string.h>
template <typename T> T** malloc_Array2D(int row, int col) { int size = sizeof(T); int point_size = sizeof(T*); T **arr = (T **) malloc(point_size * row + size * row * col); if (arr != NULL) { memset(arr, 0, point_size * row + size * row * col); T *head = (T*)((size_t)arr + point_size * row); while (row--) arr[row] = (T*)((size_t)head + row * col * size); } return (T**)arr; }
void free_Aarray2D(void **arr) { if (arr != NULL) free(arr); } int main() { printf(" C语言中动态的申请二维数组 malloc free\n"); printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n"); printf("请输入行列(以空格分开): "); int nRow, nCol; scanf("%d %d", &nRow, &nCol); int **p = malloc_Array2D<int>(nRow, nCol); int i, j; for (i = 0; i < nRow; i++) for (j = 0; j < nCol; j++) p[i][j] = i + j; for (i = 0; i < nRow; i++) { for (j = 0; j < nCol; j++) printf("%4d ", p[i][j]); putchar('\n'); } free_Aarray2D((void**)p); return 0; }
|
这里,原作者对指针的操作让我叹为观止。
但是,用这种方法创作出来的二维数组和直接int b[2][4]
得到的二维数组,结构显然是不一样的,这里我们不在进行仔细地检查。
它的具体结构我将在下一小节介绍
3.3 第三种方法
参考:https://blog.csdn.net/houqd2012/article/details/8146070
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <stdio.h> #include <stdlib.h>
int **make_array2D(int row, int col) { int **arr = (int **)malloc(row * sizeof(int *)); arr[0] = (int *)malloc(row * col * sizeof(int)); for (int i = 1; i < row; i++) arr[i] = arr[i - 1] + col; return arr; }
void free_array2D(int **arr) { free(arr[0]); free(arr); }
int main(void) { int **a = make_array2D(2, 4); for (int i = 0; i < 2; i++) for (int j = 0; j < 4; j++) a[i][j] = 10 * i + j;
for (int i = 0; i < 2; i++) { for (int j = 0; j < 4; j++) printf("%d ", a[i][j]); printf("\n"); } free_array2D(a); return 0; }
|
乍一看,这种方法生成地数组,其地址是连续地,应该和原生的二维数组是一样的(包括这种方法的原作者也是这样认为的),但如果我们仔细检查,我们会发现它其实和原生的二维数组是不一样的。
我们使用 1 中的程序对这个数组进行详细的检查。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #include <stdio.h> #include <stdlib.h>
int **make_array2D(int row, int col) { int **arr = (int **)malloc(row * sizeof(int *)); arr[0] = (int *)malloc(row * col * sizeof(int)); for (int i = 1; i < row; i++) arr[i] = arr[i - 1] + col; return arr; }
void free_array2D(int **arr) { free(arr[0]); free(arr); }
int main(void) { int **a = make_array2D(4, 2); for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) a[i][j] = 10 * i + j;
printf("sizeof(int):\t%llu\n", sizeof(int)); printf("sizeof(a):\t%llu\n", sizeof(a)); printf("sizeof(a[0]):\t%llu\n", sizeof(a[0]));
printf("\n");
printf("a:\t%p\n", a); printf("&a:\t%p\n", &a); printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("a[%d][%d]:\t%02d\n", i, j, a[i][j]);
printf("\n");
for (int i = 0; i < 4; i++) printf("a[%d]:\t%p\n", i, a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("*a[%d]:\t%d\n", i, *a[i]);
printf("\n");
for (int i = 0; i < 4; i++) printf("&a[%d]:\t%p\n", i, &a[i]);
printf("\n");
for (int i = 0; i < 4; i++) for (int j = 0; j < 2; j++) printf("&a[%d][%d]:\t%p\n", i, j, &a[i][j]); free_array2D(a); return 0; }
|
运行结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| sizeof(int): 4 sizeof(a): 8 sizeof(a[0]): 8
a: 0000000000701420 &a: 000000000064FDF0
a[0][0]: 00 a[0][1]: 01 a[1][0]: 10 a[1][1]: 11 a[2][0]: 20 a[2][1]: 21 a[3][0]: 30 a[3][1]: 31
a[0]: 0000000000701450 a[1]: 0000000000701458 a[2]: 0000000000701460 a[3]: 0000000000701468
*a[0]: 0 *a[1]: 10 *a[2]: 20 *a[3]: 30
&a[0]: 0000000000701420 &a[1]: 0000000000701428 &a[2]: 0000000000701430 &a[3]: 0000000000701438
&a[0][0]: 0000000000701450 &a[0][1]: 0000000000701454 &a[1][0]: 0000000000701458 &a[1][1]: 000000000070145C &a[2][0]: 0000000000701460 &a[2][1]: 0000000000701464 &a[3][0]: 0000000000701468 &a[3][1]: 000000000070146C
|
从上面的结果可以看出,这种方法生成的二维数组,虽然地址是连续的,但它的结构和直接int b[2][4]
得到的二维数组,还是不一样的。
它的具体结构我将在下一小节介绍
3.4 总结 : 下标运算符
首先我们介绍下标运算符,上面的三种动态分配二维数组的方法,其原理都是基于下标运算符。
下标运算符是左结合的,我们引用二维数组,通常是这样的 arr[i][j]
但其实也可以这样 (arr[i])[j]
这两种形式是等价的。
下面我们详细地描述下标运算符
ElementType[i] = *((size_t)ElementType + i * sizeof(ElementType))
可见下标运算符最重要的就是ElementType,这个ElementType 可以是内置类型 int,可以是指针类型int **
, 也可以是数组类型 int (*)[]
,在使用下标运算符时,我们要尤其注意ElementType,尤其是在我们接下来分析二维数组时。
首先我们来看内置的数组类型
a |
|
|
|
|
|
|
|
a[0] |
|
a[1] |
|
a[2] |
|
a[3] |
|
a[0] [0] |
a[0] [1] |
a[1] [0] |
a[1] [1] |
a[2] [0] |
a[2] [1] |
a[3] [0] |
a[3] [1] |
一个内置数组类型表示为int a[5][5]
,但拎出来a,他的类型是 int (*)[5]
所以arr[1]
其实是指a[1][0]
这个位置。a[1]
的类型是int *
所以(a[1])[1]
的类型就是int
接下来我们看动态分配二维数组的第一种方法:
这种方法一开始申请了一个一维数组int *a = (int *)malloc(2 * 4 * sizeof(int))
a的类型是int *
接下来我们使用强制类型转换,将a由int *
转换为int (*)[4]
,这样它就和内置的二维数组一样了。也可以使用两个下标运算符进行访问。
接下类我们看动态分配二维数组的第二种方法:
首先我们将这种方法分配的图画出来
设行数为row,列数为col,假设二维数组存储的类型为 int
第二种方法申请了 row * sizeof(int *) + row * col * sizeof(int)
字节的空间。
这个表达式的前一项row * sizeof(int *)
代表a[0] a[1] a[2] 的空间,后一项row * col * sizeof(int)
指的是连续的二维数组的存储空间。
这个程序做的就是让a[0]指向a[0] [0],让a[1] 指向 a[1] [0],依此类推。
需要注意的是这个二维数组的类型,虽然这个二维数组可以使用两个下标运算符引用,但原理却和内置的二维数组完全不同。第二种二维数组的类型是int **
而不是int (*)[col]
,也决不能是int (*)[col]
。要理解这个,我们看一下在使用下标运算符时究竟发生了什么?
首先a的类型时int **
,a[1],代表a之后一个指针大小位置的内容,即
*((size_t)a + 1 * sizeof(int *))
,就是a[1] [0]的地址,而(a[1])[0]
就是a[1] [0]的内容。
为了保证我们第一个下表运算符能够得到a[1] [0] 的地址,我们必须保证这个二维数组的类型是int **
第三种二维数组的分析与第二种类似,这里我们给出它的表示
4 指针
我们使用一个程序来展示 int (*)[col]
类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| #include <stdio.h>
void func_1(int a[][8], int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < 8; j++) printf("%d ", a[i][j]); printf("\n"); } }
void func_2(int (*a)[8], int n) { for (int i = 0; i < n; i++) { for (int j = 0; j < 8; j++) printf("%d ", a[i][j]); printf("\n"); } }
void func_3(int (*a)[8]) { for (int i = 0; i < 8; i++) printf("%d ", (*a)[i]); }
int main(void) { int q[4][8]; for (int i = 0; i < 4; i++) for (int j = 0; j < 8; j++) q[i][j] = 10 * i + j;
func_1(q, 4); printf("\n"); func_2(q, 4); printf("\n");
int p[8] = {0, 1, 2, 3, 4, 5, 6, 7}; func_3(&p); }
|
贴一篇文章 :https://blog.csdn.net/soonfly/article/details/51131141
这篇文章里对指针类型的分析很好
1 2 3 4 5 6 7 8 9
| int p; int *p; int p[3]; int *p[3]; int (*p)[3]; int **p; int p(int); Int (*p)(int); int *(*p(int))[3];
|