HOME/Articles/

C / C++ 数组指针和指针数组

Article Outline

如何正确区分指针数组数组指针


目录

  • 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中自动补全提示中是这样标注类型的的:

null


2.2 数组指针

同样借用Java语法,一个数组指针应该被写成:

int[10] (*p);

如果把int[10]理解为:一个可以存放10个整型元素的数组类型,那显然这句中p就成为了一个指针变量,并且这个指针指向一个数组

CLion中自动补全提示中是这样标注类型的:

null


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]。

根据数组和指针的关系我们知道,这里的bcd都是地址,应该是一个指针类型int *

但问题来了,a也是一个地址,也是一个指针类型,那a的类型难道也是int *?

显然不对。


我们使用C++的cout来打印一下:

cout << a << endl;

打印结果为:

0x6ffe10

这完全在我们的意料之中。


那么如果是对a进行取值呢?我们再来打印一下:

cout << *a << endl; 
// cout << a[0] << endl;也是一样

结果如下:

0x6ffe10

还是一个地址!并且和cout << a打印出来是一样的!


此时就需要引出一个名词:行指针

3.3 行指针

这张图展示了行指针的意义

null

其中,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);

其中,func1func2两种参数类型是等价的。

调用时:

func1(p, n);
func2(p, n);

4. 总结

指针数组:是一个数组,里面元素都是地址。

数组指针:是一个指针,指向整个数组,而非一个数组的起始位置。


版权声明:内容为Switernal的原创文章,遵循 CC 4.0 BY-SA 版权协议。

允许转载,转载时请附上原文链接:https://switernal.cn/2020/02/12/C和C++数组指针和指针数组/