C 数组指针

我们都知道,C 语言会为每一个变量都分配一个内存地址。数组相当于由多个变量组成的数据结构,其中每一个数组元素相当于一个变量。所以数组有多少个元素,C 语言就会为它分配多少个内存地址。

在 C 语言中,“数组指针” 和 “数组元素指针” 是 2 个不同的东西。“数组指针” 指的是整个数组开始的地址(即第 1 个元素的地址),而 “数组元素的指针” 指的是某一个元素的地址。

对于数组指针,小伙伴们记住一句话即可:不管是多少维的数组,它的指针都是第一个元素的地址。不过由于数组维度的不同,它们的指针定义语法略有不同,但本质上还是第一个元素的地址。

C 一维数组的指针

在 C 语言中,数组指针就是数组第 1 个元素的地址。所以对于一维数组来说,它的数组指针也是第 1 个元素的地址。

语法:

类型* 指针名 = 数组名;

说明:

一维数组的指针和普通变量的指针相似,也是使用同样的语法。

示例 1:使用一维数组的指针

#include <stdio.h>

int main(void)
{
    int arr[] = {10, 20, 30, 40, 50};

    int* p = arr;
    printf("%p\n", p);
    for (int i = 0; i < 4; i++)
    {
        printf("%d\n", p[i]);
    }

    return 0;
}

运行结果如下。

61FDF0
10
20
30
40

分析:

很多初学者都会有这样一个疑问: “对于变量来说,为什么需要在变量名前面加上 ‘&’ 才能获取它的内存地址,比如 int* p = &a;。但对于数组来说,为什么数组名前面不需要加上 ‘&’ ,比如int* p = arr; 呢?”

我们要记住这么一点:数组本身是多个数据的集合,它的名字本身就代表一个地址,这个地址就是该数组首元素的地址。所以对于上面这个例子来说,arr 等价于 &a[0]。我们执行下面代码就非常清楚了。

int arr[] = {10, 20, 30, 40, 50};
printf("%p\n", arr);      // 输出:61FE00
printf("%p", &arr[0]);    // 输出:61FE00

对于一维数组来说,它的数组指针变量存放的其实就是 arr[0] 的地址,所以对于这个例子来说,下面 2 种方式是等价的。小伙伴们可以自己试一下。

// 方式 1
int* p = arr;

// 方式 2
int* p = &arr[0];

需要注意的是,数组的定义以及数组指针的定义必须分开来写,而不能合并在一起写。原因就在于 {10, 20, 30, 40, 50} 是一个 “具体的值”,而不是一个 “内存地址”。

// 正确
int arr[] = {10, 20, 30, 40, 50};
int* p = arr;

// 错误
int* p = {10, 20, 30, 40, 50};

最后小伙伴们肯定还有一个疑问:“为什么使用 “p[i]” 这种方式也可以表示数组元素呢?” 实际上 p[i] 等价于 *(p+i),它本质上是一种语法糖,你可以把 p[i] 理解成 *(p+i) 的一种简写方式。之所以 C 语言会定义这种语法糖,也是为了方便我们操作数组元素而已,小伙伴们不用纠结太多。

注意: p[i] 这种省略 “*” 的方式只限用于数组,而不能用于普通变量。对于普通变量来说,想要取出它的指针对应的值,前面的 “*” 是不能省略的。

最后来总结一下,如果想要访问一维数组的元素,我们有以下 2 种方式。

  • 下标法:arr[i] 或 p[i]。
  • 指针法:*(p+i)。

C 数组指针的算术运算

对于 C 指针来说,我们也可以对它进行算术运算。指针的算术运算只有以下 3 种情况。

  • 指针加上整数。
  • 指针减去整数。
  • 两个指针相减。

两个指针相减在初学阶段较少见到,这里重点介绍以下指针的加法和减法。

示例 2:一维数组的指针运算

#include <stdio.h>

int main(void)
{
    int arr[] = {10, 20, 30, 40, 50};
    int* p = arr;

    printf("%p\n", p);
    printf("%p\n", p + 1);
    printf("%p", p + 2);
    return 0;
}

运行结果如下。

61FE00
61FE04
61FE08

分析:

指针的加减法运算和普通整数的加减法是不一样的。比如有普通整数 10,它加上 1 之后,就应该是 11。但如果有一个整型指针 61FDF0,它加上 1 之后,并不是 61FDF1,而是 61FDF4。

之所以会这样,是因为指针前进的步长和它指向的数据类型有关。对于上面例子来说,指针指向的数据类型是 int,也就是 4 个字节。“p + 1” 并不是对地址值进行加 1,而是将 p 前进 1 步。

有了上面的了解,接下来理解 *p、*(p + 1) 这些就非常简单了。*p 等价于 *(p + 0),也就是获取第 1 个元素的值。而 *(p + 1) 表示将指针 p 前进一位,然后获取前进之后的地址所对应的值,也就是第 2 个元素的值。

printf("%p\n", *p);            // 10
printf("%p\n", *(p + 1));      // 20
printf("%p\n", *(p + 2));      // 30

对于指针的加减法,不仅仅适用于一维数组,同样也适用于二维数组,我们在后面会慢慢接触到。

示例 3:字符串的指针运算

#include <stdio.h>
#include <string.h>

int main(void)
{
    char str[] = "lvye";
    char* p = str;

    int length = strlen(str);
    for (int i = 0; i < length; i++)
    {
        printf("%c\n", p[i]);
    }

    return 0;
}

运行结果如下。

l
v
y
e

分析:

字符串本质上是一个一维数组(即字符数组),所以它的指针其实也是一维数组的指针。此外,由于字符串本质上是一个字符数组,所以 p 前面的类型应该是 char*,而不是 int*,这一点小伙伴要搞清楚。

字符串是一种特殊的一维数组,字符串的定义以及字符串指针的定义不需要分开写,而完全可以直接定义一个指针指向一个字符串。也就是说,下面方式 1 完全可以简写为方式 2。

// 方式 1
char str[] = "lvye";
char* p = str;

// 方式 2
char* p = "lvye";

最后还要说明一点,字符串的名字本身就代表该数组的地址,当我们使用 scanf() 输入一个字符串时,其实字符串名前面是不需要加上 “&” 的。但实际上,字符串名加不加 “&” 都是可行的,编译都不会报错。下面 2 种方式都是可行的,小伙伴们可以试一下。

// 方式 1
scanf("%s", str);

// 方式 2
scanf("%s", &str);

C 二维数组的指针

在 C 语言中,不管是多少维的数组,它的指针都是第 1 个元素的地址。所以对于二维数组来说,它的指针同样也是第 1 个元素的地址。

虽然一维数组和二维数组这两个的指针都是第 1 个元素的地址,但这两种数组指针定义的语法是不一样的。

语法:

int (*p)[n] = arr;

说明:

n 是每一行元素的个数,而不是行数。此外 *p 外面的 “()” 不能省略,如果写成 int* p[n] = arr; 就是错误的。

示例 4:使用二维数组的指针

#include <stdio.h>

int main(void)
{
    int arr[2][3] = {{10, 20, 30}, {40, 50, 60}};

    int (*p)[3] = arr;
    printf("%p\n", p);

    for (int i = 0; i < 2; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("%d\n", p[i][j]);
        }
    }
    return 0;
}

运行结果如下。

61FDF0
10
20
30
40
50
60

分析:

首先小伙伴们要重点搞清楚 int (*p)[3] = arr; 这一句代码是什么意思。对于二维数组来说,我们必须使用 int (*p)[n] = arr; 这种方式,而不能使用 int* p = arr; 这种方式。这里的 n 并不是行数,而是列数,也就是每一行的元素个数,这一点大家就不要搞错了。此外,不管多少维的数组,数组名代表的都是第 1 个元素的地址,这是非常重要的一个点。

对于这个例子来说,我们可以分开来写,下面 2 种方式是等价的。

// 方式 1
int arr[2][3] = {{10, 20, 30}, {40, 50, 60}};
int (*p)[3] = arr;

// 方式 2
int arr[2][3] = {{10, 20, 30}, {40, 50, 60}};
int (*p)[3];
p = arr;

对于二维数组来说,它的数组指针变量存放的其实是第 0 行的开始地址。其中 &arr[0] 是第 1 行的开始地址,而 &arr[n-1] 是第 n 行的开始地址。所以对于这个例子来说,下面 2 种方式是等价的。小伙伴们可以自己试一下。

// 方式 1
int (*p)[3] = arr;

// 方式 2
int (*p)[3] = &arr[0];

同样地,我们可以使用 p[i][j] 这种方式来获取数组元素的值。p[i][j] 等价于 *(*(p+i)+j) 或 *(p[i]+j),它同样是 C 语言为了方便我们获取数组元素的一种语法糖。

最后,如果想要访问二维数组的元素,我们同样有以下 2 种方式。

  • 下标法:arr[i][j] 或 p[i][j]。
  • 指针法:*(*(p+i)+j)。

示例 5:使用二维数组指针打印第 n 行元素

#include <stdio.h>

int main(void)
{
    int arr[3][4] = {{2, 4, 6, 8}, {10, 12, 14, 16}, {18, 20, 22, 24}};
    int (*p)[4] = arr;

    for (int i = 0; i < 4; i++)
    {
        printf("%d\n", p[2][i]);
    }
    return 0;
}

运行结果如下。

18
20
22
24

分析:

对于这个例子来说,p[2][i] 等价于 *(p[2]+i) 或者 *(*(p+2)+i),小伙伴们可以自行测试一下,然后理解一下这几种方式。

虽然二维数组在概念上是二维的,但是它所有元素在内存中却是连续排列的,元素与元素之间没有任何 “缝隙”。

int a[3][4] = {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}};

如果从概念上来理解,它在内存分布应该是二维的,如下所示。

0   1   2   3
4   5   6   7
8   9  10  11

但实际上它在内存的分布是一维线性的,整个二维数组占据的是一块连续的内存,如下图所示。

C二维数组的存储结构

对于二维数组来说,我们记住这么一句话就可以了:二维数组在逻辑上是二维的,在物理上是线性连续的。

示例 6:二维数组的结构

#include <stdio.h>

int main(void)
{
    int arr[2][3] = {{10, 20, 30}, {40, 50, 60}};
    printf("%p\n", arr);
    printf("%p\n", arr[0]);
    printf("%p", arr[1]);
    return 0;
}

运行结果如下。

61FE00
61FE00
61FE0C

分析:

arr 是一个数组名,它本身代表的就是首元素的地址,也就是第 1 行第 1 个元素的地址。arr[0] 表示的第 1 行第 1 个元素的地址,arr[1] 表示的第 2 行第 1 个元素的地址,以此类推。

深入 C 数组指针

在 C 语言中,如果想要借助数组指针变量来获取数组中的某一个元素,我们需要清楚以下 2 点。

  • 对于一维数组来说,我们需要借助 1 个 “*” 或 1 个 “[]” 才能获取某一个元素。
  • 对于二维数组来说,我们需要借助 2 个 “*” 或 2 个 “[]” 才能获取某一个元素。或者 1 个 “*” 加上 1 个 “[]” 的组合也可以。

示例 7:一维数组的指针

#include <stdio.h>

int main(void)
{
    int arr[] = {10, 20, 30, 40, 50};
    int* p = arr;

    printf("%d\n", *p);
    printf("%d\n", *(p + 1));
    printf("%d\n", *(p + 2));

    return 0;
}

运行结果如下。

10
20
30

分析:

*p 等价于 *(p+0),所以它获取的是第 1 个元素。对于一维数组来说,如果想要获取下标为 i 的元素,我们有以下 2 种方式。

p[i]        // 下标
*(p+i)      // 指针

示例 8:二维数组的指针

#include <stdio.h>

int main(void)
{
    int arr[2][3] = {{10, 20, 30}, {40, 50, 60}};
    int(*p)[3] = arr;

    printf("%d\n", **p);
    printf("%d\n", *(*(p + 0) + 1));
    printf("%d\n", *(*(p + 0) + 2));

    return 0;
}

运行结果如下。

10
20
30

分析:

对于二维数组来说,我们需要 2 层的 “*” 才能获取到某一个元素。**p 等价于 *(*(p+0)+0),所以它获取的是第 1 行第 1 个元素。对于二维数组来说,如果想要获取某一个元素,我们有以下 3 种方式。

p[i][j]          // 下标
*(*(p+i)+j)      // 指针
*(p[i]+j)        // 下标 + 指针

不管是哪一种方式,我们只需要保证 “*” 和 “[]” 使用的次数加起来是 2 就可以了,因为这是一个二维数组嘛。

此外,当 i 或 j 为 0 时,我们是可以把 i 和 j 省略掉的。小伙伴们要了解下面几种情况,因为在很多地方都会接触到。

  • *(*(p+0) + 0) 等价于 **p。
  • *(*(p+0) + 1) 等价于 *( *p + 1 )。
  • *(*(p+1) + 0 ) 等价于 *( *(p + 1) )。

C 数组指针的常见问题

1. 为什么二维数组的指针定义的语法,和一维数组的指针定义语法不一样呢?难道都统一成 “类型* 指针名 = 地址;” 这种方式不是很好吗?

首先我们要知道,数组指针其实又叫做 “行指针”。对于一维数组来说,它只有一行。我们只需要拿到首元素的地址,往后递增(如 p + 1)就可以拿到下一个元素的地址。

C 数组指针效果图 1

但是对于二维数组来说,所有元素的地址是连续的,但是二维数组是可以有多行的,那么我们怎么知道哪一个地址是第 1 行首元素的地址,哪一个地址是第 2 行首元素的地址呢?此时我们在定义数组指针的时候,必须告诉计算机每一行有多少个元素,然后计算机就会自动划分了。

例如 int (*p)[3]; 就是定义一个二维数组的指针,该数组每一行有 3 个元素,这样我们就可以获取每一行首元素的地址了。其中 &p[0] 就是第 1 行首元素的地址,&p[1] 就是第 2 行首元素的地址,以此类推。

C 数组指针效果图 2

现在小伙伴应该能明白,为什么这两种数组指针定义的语法不一样了吧?总而言之,不管是什么类型的指针,它指向的也仅仅是一个地址。但是由于定义语法的不同,使得它可以拥有不同的特性。C 语言就是根据定义语法的不同,赋予不同指针不同特性的。

上一篇: C 指针

下一篇: C 指针数组

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号