如何正确区分指针数组和数组指针?
目录
- TOC {:toc}
0. 引言
来看下面两个情况:
int *p[10]; //指针数组
和
int (*p)[10]; //数组指针
指针数组和数组指针听起来相当绕口,难以区分,稍不留心就会搞错。
1. 从符号优先级角度理解
虽然查阅了一些资料,但几乎没有文章能够指出问题的本质所在,所以参考了几篇博客,作以下简要理解和分析,方便理解和记忆。
1.1 指针数组
根据C和C++的优先级(可以参考优先级的文章)我们很容易知道,
数组下标中括号 []
的优先级高于指针运算符 *
在
int *p[10];
中,p
先与[10]
结合,组成p[10]
,和很容易看出这是一个数组的定义;
再向前看,int *
则指明了数组p[10]
中元素的类型是一个指针类型。
因此,上面一行代码定义了一个指针数组。
1.2 数组指针
而在下面一行代码中,
int (*p)[10];
出现了小括号( ),而小括号( )和中括号[ ]同处于最高优先级,最高优先级结合方式为从左向右,
因此编译器先看到了(*p)
,这是一个指针变量的定义;
随后再后面的[10]
结合。
这仍令人十分困惑,因为(*p)[10]
怎么看都像一个数组啊!
2. 借助Java语法加以理解
在Java中,我们可以使用以下两种方法声明一个整型数组变量:
int array[];
或
int[] array;
在Java教程中,更推荐使用后者,即使用int[] array
来声明。
我们可以把int[]
理解为一个新类型,即数组类型。
但请注意,这种写法在C和C++中并不合法。
2.1 指针数组
如果借用Java第二种声明数组变量的语法,我们重新声明一下指针数组,那么写出来应该是这样:
int*[10] p;
此时,如果把int
和*
组合在一起,将int*
理解为一个指针类型,写成这样:
(int*)[10] p;
那么显然p
是一个数组,里面存放了10个指向整数类型数据的指针。
CLion中自动补全提示中是这样标注类型的的:
2.2 数组指针
同样借用Java语法,一个数组指针应该被写成:
int[10] (*p);
如果把int[10]
理解为:一个可以存放10个整型元素的数组类型,那显然这句中p
就成为了一个指针变量,并且这个指针指向一个数组。
CLion中自动补全提示中是这样标注类型的:
3. 进一步理解数组指针
指针数组不难理解,无非是在一个塞满了指针的数组,每一个元素都是一个地址。
那么数组指针呢?
如果你在IDE中写出下列代码:
int a[10];
int (*p)[10] = a;
会出现编译错误:
[Error] cannot convert 'int*' to 'int (*)[10]' in initialization.
为什么?指针p
指向了一个数组a
,没错啊?
3.1 数组名和指针的关系
的确,上面代码中,a
是一个数组名,也的确是一个地址。
但问题在于,单独使用a
,指向的是数组的起始地址,并非指向整个数组。
这个概念有些抽象。
3.2 引入二维数组的行指针
下面声明一个二维数组:
int a[3][4];
如果线性理解二维数组,可以看作是一个存放了三个元素的数组a[3]
,而这三个元素的类型都是数组类型,而这三个数组每个数组中存放了4个整型数据,我们称这三个数组为b[4],c[4], d[4]。
根据数组和指针的关系我们知道,这里的b
、c
、d
都是地址,应该是一个指针类型int *
。
但问题来了,a
也是一个地址,也是一个指针类型,那a
的类型难道也是int *
?
显然不对。
我们使用C++的cout来打印一下:
cout << a << endl;
打印结果为:
0x6ffe10
这完全在我们的意料之中。
那么如果是对a
进行取值呢?我们再来打印一下:
cout << *a << endl;
// cout << a[0] << endl;也是一样
结果如下:
0x6ffe10
还是一个地址!并且和cout << a
打印出来是一样的!
此时就需要引出一个名词:行指针
3.3 行指针
这张图展示了行指针的意义
其中,a[0]、a[1]、a[2]指向的是整个数组,即一整块内存,而我们新定义的b、c、d则仅仅指向三个数组的起始位置。
我们理解了,数组指针本质上是一个指针,这个指针指向一个一维数组。
这样我们可以写出下列代码:
int a[3][4];
int (*p)[4] = a;
//常见以下写法:
/*
int (*p)[4];
p = a;
*/
此时编译器就不会再报错了。
上述代码中,a
就是一个数组指针,同时也是一个行指针,指向二维数组的第一行。
3.3.1 数组指针代码演示 (C++)
#include <iostream>
using namespace std;
int main() {
int a[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,-999} };
int (*p)[4];
/* 移动p指针 */
p = a // p指向了二维数组a的第1行
cout << "p指向第1行地址:" << p << endl;
p++; // 现在p指向了第2行
cout << "p指向第2行地址:" << p << endl;
p = p + 1; // 现在p指向了第3行
cout << "p指向第3行地址:" << p << endl;
p -= 2 // p又移回了a的第1行
cout << "p移回第1行地址:" << p << endl;
/* 取值 */
int b;
b = **p; // b现在等于a的第1行第1列元素
cout << "第1行第1列元素:" << b << endl;
b = *(*(p+1)+2)) // b现在等于第2行第3列元素
cout << "第2行第3列元素:" << b << endl;
b = *(*(p+2)+1)) // b现在等于第3行第2列元素
cout << "第3行第2列元素:" << b << endl;
return 0;
}
3.3.2 重点分析
我们来着重分析一下这一行代码:
b = *(*(p+2)+1)) // b现在等于第3行第2列元素
首先,根据运算符优先级,最先计算的是(p+2)
。由于p
指向a
的第1行,因此p+2
指向a
的第3行。
然后,与取值运算符*
结合,*(p+2)
取出的实际上是一个一维数组,也就是取出了整个第3行。
再而,*(p+2) + 1
,上面说了,取出了一整行,因此这里的+ 1
是在第3行内,指针向右移动一位,指向第2个元素
最后,再使用一次取值运算符*
,使得*(*(p+2)+1)
取出了第3行第2个元素。
3.4 数组指针的实际意义
使用数组指针作为函数的参数,可以将整个二维数组传入函数
有如下声明:
int n = 0;
int a[3][4];
int (*p)[4] = a;
void func1( int (*a)[4], int b );
void func2( int a[][4], int b);
其中,func1
和func2
两种参数类型是等价的。
调用时:
func1(p, n);
func2(p, n);
4. 总结
指针数组:是一个数组,里面元素都是地址。
数组指针:是一个指针,指向整个数组,而非一个数组的起始位置。
版权声明:内容为Switernal的原创文章,遵循 CC 4.0 BY-SA 版权协议。
允许转载,转载时请附上原文链接:https://switernal.cn/2020/02/12/C和C++数组指针和指针数组/