我们都知道,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但实际上它在内存的分布是一维线性的,整个二维数组占据的是一块连续的内存,如下图所示。

对于二维数组来说,我们记住这么一句话就可以了:二维数组在逻辑上是二维的,在物理上是线性连续的。
示例 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)就可以拿到下一个元素的地址。

但是对于二维数组来说,所有元素的地址是连续的,但是二维数组是可以有多行的,那么我们怎么知道哪一个地址是第 1 行首元素的地址,哪一个地址是第 2 行首元素的地址呢?此时我们在定义数组指针的时候,必须告诉计算机每一行有多少个元素,然后计算机就会自动划分了。
例如 int (*p)[3]; 就是定义一个二维数组的指针,该数组每一行有 3 个元素,这样我们就可以获取每一行首元素的地址了。其中 &p[0] 就是第 1 行首元素的地址,&p[1] 就是第 2 行首元素的地址,以此类推。

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