二重指针详解
0赞二重指针详解
朱有鹏
1.二重指针
1.1、二重指针与普通一重指针的区别
本质上来说,二重指针和一重指针的本质都是指针变量,指针变量的本质就是变量。
一重指针变量和二重指针变量本身都占4字节内存空间,
1.2、二重指针的本质
(1)二重指针本质上也是指针变量,和普通指针的差别就是它指向的变量类型必须是个一重指针。二重指针其实也是一种数据类型,编译器在编译时会根据二重指针的数据类型来做静态类型检查,一旦发现运算时数据类型不匹配编译器就会报错。
(2)C语言中如果没有二重指针行不行?其实是可以的。一重指针完全可以做二重指针做的事情,之所以要发明二重指针(函数指针、数组指针),就是为了让编译器了解这个指针被定义时定义它的程序员希望这个指针被用来指向什么东西(定义指针时用数据类型来标记,譬如int *p,就表示p要指向int型数据),编译器知道指针类型之后可以帮我们做静态类型检查。编译器的这种静态类型检查可以辅助程序员发现一些隐含性的编程错误,这是C语言给程序员提供的一种编译时的查错机制。
(3)为什么C语言需要发明二重指针?原因和发明函数指针、数组指针、结构体指针等一样的。
1.3、二重指针的用法
(1)二重指针指向一重指针的地址
(2)二重指针指向指针数组的
(3)实践编程中二重指针用的比较少,大部分时候就是和指针数组结合起来用的。
(4)实践编程中有时在函数传参时为了通过函数内部改变外部的一个指针变量,会传这个指针变量的地址(也就是二重指针)进去
1.4、二重指针与数组指针
(1)二重指针、数组指针、结构体指针、一重指针、普通变量的本质都是相同的,都是变量。
(2)所有的指针变量本质都是相同的,都是4个字节,都是用来指向别的东西的,不同类型的指针变量只是可以指向的(编译器允许你指向的)变量类型不同。
(3)二重指针就是:指针数组指针
2、二维数组
2.1、二维数组的内存映像
一维数组在内存中是连续分布的多个内存单元组成的,而二维数组在内存中也是连续分布的多个内存单元组成的。从内存角度来看,一维数组和二维数组没有本质差别。如:二维数组int a[2][5]和一维数组int b[10]对应关系如下:
a[0][0] a[0][1] a[0][4] a[1][0] a[1][1] a[1][4]
b[0] b[1] b[4] b[5] b[6] b[9]
既然二维数组都可以用一维数组来表示,那二维数组存在的意义和价值在哪里?明确告诉大家:二维数组a和一维数组b在内存使用效率、访问效率上是几乎相同。使用用二维数组而不用一维数组,原因是在某些情况下,二维数组更好理解、利于组织。我们使用二维数组,并不是必须,而是一种简化编程的方式。一维数组的出现其实也不是必然的,也是为了简化编程。
2.2、二维数组的识别之第一维和第二维
二维数组必然是两个维度,假设数组为a[2][5]从左到右看:
[2]是第一个维度,表示a这个数组里有两个元素。
[5]是第二个维度,需要进入第一维度的内部观察。它的内部有5个int型的元素。
图 数组为a[2][5]内部模型
对后面小节的理解,请读者结合上图进行理解。
2.3、数组名代表数组首元素的地址
“数组名代表数组首元素的地址”这句话既适用于一维数组,也适用于二维数组。
对于一维数组int a[5]而言:数组名a就表示首元素a[0]的地址,及数组名a等价于&a[0]。
对于二维数a[2][5]组而言:数组名a就表示首元素a[0]的地址,及数组名a等价于&a[0]。
接着看a[0],此时的a[0]有两重身份:在二维数组的第一个维度里a[0]是数组的首元素;而在第二维度里,a[0]本身就是个数组,该数组的首元素是a[0][0],所以此时a[0]也代表一个数组名。(本段结论同样适合a[1])。
通过“数组名代表数组首元素的地址”可知,a[0]等价于&a[0][0]。而前面同时有a等价于&a[0]的结论,所以可以得到a等价于&&a[0][0]。记住这个结论,对后面理解数组指针访问二维数组的方式大有助益。
2.4、指针访问二维数组的两种方式
2.4.1、普通指针指向二维数组的第一维
还是拿数组a[2][5]举例,在第一维度里,该数组有两个元素,分别是a[0]和a[1],而a[0]和a[1]本身是个一维数组,及a[0]和a[1]就是数组的数组名,所以我们可以像访问普通的一维数组那样访问a[0]和a[1]:
int* p1 = a[0];//数组名代表数组首元素的地址
int* p2 = a[1];//数组名代表数组首元素的地址
printf("a[0][0] = %d.\n", *p1); //*p1对应的是a[0][0]的值
printf("a[0][1] = %d.\n", *(p1+1)); //*(p1+1)对应的是a[0][1]的值
printf("a[1][0] = %d.\n", *p2); //*p2对应的是a[1][0]的值
printf("a[1][1] = %d.\n", *(p2+1)); //*(p2+1)对应的是a[1][1]的值
2.4.2、数组指针访问二维数组
终于到了本章节的重点,前面的内容都是这节的铺垫。
比如:int a[3][3]这样一个二位数组来说,通过前面的学习,我们已经知道,a[3][3]可以分为如下三个小一维数组,
a[0][0] a[0][1] a[0][2]
a[1][0] a[1][1] a[1][2]
a[2][0] a[2][1] a[2][2]
当a即是二维数组名称,但是同时a也表示二维数组的第一个小一维数组(a[0][0] a[0][1] a[0][2])的整个数组的数组首地址,a等价于a[0],那么一问就来了,什么样的指针才能存放数组首地址呢?答案就是数组指针。
如二维数组int a[2][5],能指向改二维数组的数组指针类型为int (*)[5],需要注意的是数组指针类型中的5不是乱填的,它的值必须和他指向的二维数组的第二维中的元素相等。如 char b[77][9]那么此时需要的数组指针类型为char (*)[9]。
了解数组指针的选型之后,就来讲解数组指针访问二维数组的过程,还以int a[2][5]为例:
int (*p)[5] = a;//数组指针p指向二维数组a[2][5]
那么如何通过p来访问a[2][5]中的元素呢:
printf("a[0][0] = %d.\n",**p); //**p对应的是a[0][0]的值
printf("a[0][1] = %d.\n",*(*p+1)); //*(*p+1)对应的是a[0][1]的值
printf("a[0][4] = %d.\n",*(*p+4)); //*(*p+1)对应的是a[0][4]的值
printf("a[1][0] = %d.\n",**(p+1)); //**(p+1)对应的是a[1][0]的值
printf("a[1][1] = %d.\n",*(*(p+1)+1));//*(*(p+1)+1)对应的是a[1][1]的值
printf("a[1][4] = %d.\n",*(*(p+1)+4));//*(*(p+1)+4)对应的是a[1][4]的值
很多人对p要进行两次解引用才能得到值,表示不理解,如果读者能想到前面提到的结论,可能会恍然大悟。前面结论提到——当a是二维数组的数组名时,a等价于&&a[0][0]。首先,a和数组指针p是类型匹配的;其次“解引用(*)”和“取地址(&)”这个两个过程是逆过程。之前a[0][0]连续取两次地址才和a等价,那现在p连续两次解引用得到a[0][0]也是理所当然(**p对应的是a[0][0]的值)。