来深入理解一下数组与指针吧~
在C 语言函数指针的一些理解 中已经梳理过了函数指针、指针数组以及二者结合的使用情形,现在继续深入理解一下数组与指针之间的差异与联系。
回顾 首先,看一段代码回顾一下数组与指针的常规使用:
test1 1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <stdlib.h> int main () { int arr[] = {1 , 2 , 3 }, *pa = arr; for (int i = 0 ; i < sizeof (arr) / sizeof (*arr); i++) printf ("%d\n" , *pa++); return 0 ; }
类似的,还有下面这样的用法:
test2 1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <stdlib.h> int main () { int arr[] = {1 , 2 , 3 }, *pa = arr; for (int i = 0 ; i < sizeof (arr) / sizeof (*arr); i++) printf ("%d\n" , pa[i]); return 0 ; }
以及,这样的用法:
test3 1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <stdlib.h> int main () { int arr[] = {1 , 2 , 3 }, *pa = arr; for (int i = 0 ; i < sizeof (arr) / sizeof (*arr); i++) printf ("%d\n" , *(pa + i)); return 0 ; }
以上这些用法,都是比较容易理解的指针在数组上的简单应用。 下面,再来仔细研究一下数组与指针的关系。
一维数组 在之前的文章里面,已经提到过了数组名与指针指的不是同一种东西,用来区分二者不同的工具是sizeof关键字,也就是下面这段代码:
test4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> int main () { int arr[] = {1 , 2 , 3 }, *pa = arr; printf ("arr = %p, pa = %p\n" , arr, pa); printf ("sizeof(arr) = %d\n" , sizeof (arr)); printf ("sizeof(pa) = %d\n" , sizeof (pa)); return 0 ; }
显然,从上面的输出结果中可以知道arr这个数组名占了 12 个字节,也就是它总元素个数乘以单位元素的大小;同时,我们也知道了一个普通int *指针所占大小是 4。尽管它们值都是地址且相同,但二者是完全不一样的。
实际上,按照 C 语言的规定,数组名是一个常量 ,并且其值是一个地址,但并不能将数组名与指针等价。 在前面的代码中,使用了一个int *指针来访问数组的元素,实际上这个指针指向的是数组的元素。如果,需要有一个指针指向数组,那么需要按照如下的方式声明指针:
test5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> #include <stdlib.h> int main () { int arr[3 ] = {1 , 2 , 3 }; int (*parr)[3 ] = &arr; printf ("arr = %p\n" , arr); printf ("parr = %p\n" , parr); return 0 ; }
这与之前讨论函数指针的用法时是一致的,也就是需要使用()将指针名与*括起来,从而告诉编译器,这是一个指针,而不是指针数组。 但此时需要注意的是,parr是一个数组指针,并不能像使用pa一样遍历数组。而pa之所以能遍历数组的原因是因为 C 语言中存在一个叫做偏移量 的概念,在声明了pa是一个int *后,每次执行pa + 1,那么pa执行的地址就会移动 4 个字节,这就是偏移量(offset)。
二维数组 现在,再回头看二维数组。 我们先声明一个二维数组,并输出对应的地址:
test6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; printf ("arr = %p\n" , arr); printf ("*arr = %p\n" , *arr); printf ("**arr = %d\n" , **arr); return 0 ; }
可以发现,arr和*arr表示的值都是一样的地址,也即数组首元素的地址;并且arr这个数组名需要进行两次解引用操作,才可以表示数组的元素。 实际上,arr就是在 test5 中声明的数组指针,我们可以用下面的代码来验证:
test7 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; printf ("arr = %p\n" , arr); printf ("arr + 1 = %p\n" , arr + 1 ); printf ("**arr = %d\n" , **arr); printf ("**(arr + 1) = %d\n" , **(arr + 1 )); return 0 ; }
在上面的代码中,借用了一维数组与指针的技巧,将arr指针移动了一个偏移量,得到了两个不同的地址。。而两个地址的差值:$0061FEFC_{16} - 0061FF08_{(16)} = C_{(16)} = 12_{(10)}$,这个 12 正好就是 3 个int的大小,所以**(arr + 1) = 4。
现在,我们就可以写出下面的代码了:
test8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; int (*parr)[3 ] = arr; for (int i = 0 ; i < 3 ; i++) { for (int j = 0 ; j < 3 ; j++) printf ("%d " , parr[i][j]); putchar ('\n' ); } return 0 ; }
或者是:
test9 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; int (*parr)[3 ] = arr; for (int i = 0 ; i < 3 ; i++) { for (int j = 0 ; j < 3 ; j++) printf ("%d " , *(*(parr + i) + j)); putchar ('\n' ); } return 0 ; }
类似的用法,其实都是差不多的。
数组传参 接下来,我们在考虑如何在函数中传入数组的问题。 就一维数组而言,直接利用一个指针即可,比如:
test10 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> void fun (int *a, int size) { for (int i = 0 ; i < size; i++) printf ("%d " , *(a + i)); putchar ('\n' ); } int main () { int arr[3 ] = {1 , 2 , 3 }; fun(arr, 3 ); return 0 ; }
函数的声明也可以写成void fun(int a[], int size);,这样写其实更新醒目。 同样,二维数组也类似,但是需要指定第二维的大小,也就是:
test11 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> void fun (int (*arr)[3 ]) { for (int i = 0 ; i < 3 ; i++) { for (int j = 0 ; j < 3 ; j++) printf ("%d " , *(*(arr + i) + j)); putchar ('\n' ); } } int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; fun(arr); return 0 ; }
同样,函数的声明也可以写成void fun(int arr[][3]);。
下面,我们来做一件有趣的事情,先看代码:
test12 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> void fun (int *arr) { for (int i = 0 ; i < 9 ; i++) printf ("%d " , *(arr + i)); putchar ('\n' ); } int main () { int arr[3 ][3 ] = {{1 , 2 , 3 }, {4 , 5 , 6 }, {7 , 8 , 9 }}; fun(arr); return 0 ; }
在上面的代码中,我们把二维数组的首地址,当作一维数组的首地址传给函数,尽管编译有 warnings,但是程序运行的结果是符合预期的。为什么会出现这种现象呢?实际上,二维数组在内存中的存储方式与一维数组别无二致,都是一个接一个顺序排列的,这也就意味着我们可以利用前面提到的“偏移量”这个概念来直接读取内存,达到我们的目的。 这就是 C 语言区别于 Java 这类语言的地方,它允许程序员自己操作内存,但前提是你要知道自己在干什么,会得到什么样的效果,是否符合预期。同时,上面这个例子,也告诉我们,程序中的 warnings,不可忽略,稍不注意就会产生错误。
字符数组 下面,再单独看看字符指针数组,先看下面的代码:
test13 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> int main () { char *str1 = "How" ; char str2[5 ] = "How" ; printf ("str1 = %s\n" , str1); printf ("str2 = %s\n" , str2); str1 = "are" ; printf ("str1 = %s\n" , str1); return 0 ; }
前面已经提到过数组名是一个常量 ,所以这里的str2就不能直接被赋值,但是str1仅仅只是一个指针,它是可以被赋值的。实际上,这个过程也很容易想明白,重新赋值str1无非是要让这个指针再指向另外一块内存即可,但重新赋值str2意味着要在内存中找一块一样大小的空间,并写入与原先相同的值,这是很复杂的事情。
尽管字符数组无法直接赋值,但字符指针数组是可以直接赋值的,比如:
test14 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <stdlib.h> int main () { char *str[4 ] = {"I" , "am" , "a" , "cat." }; for (int i = 0 ; i < 4 ; i++) printf ("%s " , str[i]); putchar ('\n' ); str[3 ] = "mouse." ; for (int i = 0 ; i < 4 ; i++) printf ("%s " , str[i]); putchar ('\n' ); return 0 ; }
总结起来,也就是一句话:**数组名无法直接被赋值,但指针可以直接被赋值。**在进行赋值操作时,注意区分数组名和指针即可。
结尾 到这里,有关函数、指针和数组之间的基本理解,基本上已经说完了。其他与指针、函数和数组组合一起使用的东西本质上也就是多层概念的嵌套使用,如果基本概念扎实,仔细分析之后也能得出结果。比如下面两种:
test15 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #include <stdlib.h> int main () { int *(*funcp[3 ])(int ); printf ("sizeof(funcp) = %d\n" , sizeof (funcp)); int (*(*funcp2)[3 ])(int ); printf ("sizeof(funcp2) = %d\n" , sizeof (funcp2)); exit (0 ); }
根据sizeof关键字,我们可以知道,funcp是指向指针函数的函数指针数组,funcp2是指向函数指针数组的指针。不得不说,指针真的是个强大的东西。尽管如此,真正写代码的时候这样写,大概率会被打的很惨🤣~尽量保持简单、可读性高的代码才是比起炫技更应该值得考虑的事情。
OK,到此结束,后续想到了再补充~