16、指针(二):指针与数组的关系、通过指针访问数组元素、指针的算术运算

好,咱们接着聊指针。上一章我把指针的基本概念和用法讲清楚了,这一章咱们要深入一个非常关键的话题——指针和数组到底是什么关系

说实话,我当年学C语言的时候,最困惑的就是这个。数组不就是一堆连续的内存单元吗?指针不就是个地址吗?它们俩怎么就扯上关系了?后来在项目中踩了几次坑,才真正明白——数组名本质上就是个指针常量。嗯,这句话你记住了,后面就好办了。

数组名:它到底是不是指针?

先看一段最简单的代码:

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;   // 数组名直接赋值给指针

你看,arr 可以直接赋值给 p。这说明什么?数组名就是数组首元素的地址。说白了,arr 等价于 &arr[0]

但这里有个坑——数组名不是变量,你不能给它赋值。比如:

int arr[5];
arr = NULL;   // 编译错误!
arr++;        // 编译错误!

为什么会这样?因为数组名是一个指针常量,它的值在编译时就固定了。你想想看,如果数组名可以被修改,那编译器怎么知道数组的起始地址?所以,数组名是常量,指针变量是变量,这是本质区别。

核心结论:

  • arr 等价于 &arr[0]
  • arr 是常量,不能修改
  • p 是指针变量,可以指向别处

通过指针访问数组元素

既然指针可以指向数组首元素,那怎么通过指针访问数组里的每个元素呢?

最直接的方式——下标法

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

for (int i = 0; i < 5; i++) {
    printf("%d ", p[i]);   // 用指针当数组用
}

你看,p[i]arr[i] 完全等价。编译器会把 p[i] 翻译成 *(p + i)。所以,下标操作本质上就是指针运算的语法糖

另一种方式——指针偏移法

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

for (int i = 0; i < 5; i++) {
    printf("%d ", *(p + i));   // 等价于 p[i]
}

我个人习惯用下标法,因为可读性更好。但在某些场景下,指针偏移法更灵活。比如你要遍历一个数组,但又不想改变指针本身的值:

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

while (p < arr + 5) {
    printf("%d ", *p);
    p++;   // 指针移动,p 的值变了
}

这里 p 在循环中不断自增,每次指向下一个元素。注意循环条件 p < arr + 5arr + 5 是数组末尾的下一个位置,我们称之为越界指针,它不指向任何有效元素,但可以用来做边界判断。

小技巧: 如果你只需要读取数组,不想修改指针,可以用一个临时指针来遍历:

int *tmp = arr;
while (tmp < arr + 5) {
    printf("%d ", *tmp++);
}

这样原指针 arr 不会被改变。

指针的算术运算

指针的加减运算,说白了就是移动指针指向的位置。但这里有个关键点——指针加减的是元素的大小,不是字节数

举个例子:

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

printf("p = %p\n", p);       // 假设是 0x1000
printf("p + 1 = %p\n", p + 1); // 0x1004,不是 0x1001
printf("p + 2 = %p\n", p + 2); // 0x1008

你看,p + 1 跳过了 4 个字节(因为 int 占 4 字节)。指针加 1,地址增加的是 sizeof(类型) 个字节。这个规则适用于所有指针类型。

指针支持的算术运算包括:

  • 指针 + 整数:指向后面的第 n 个元素
  • 指针 - 整数:指向前面的第 n 个元素
  • 指针 - 指针:得到两个指针之间的元素个数(必须指向同一数组)
  • 自增/自减:++p、p++、--p、p--

我记得有一次在项目中,我需要计算两个指针之间有多少个元素,直接用了减法:

int arr[10];
int *start = &arr[2];
int *end = &arr[7];
int count = end - start;   // 结果是 5

这个特性非常实用,尤其是在处理缓冲区或字符串时。

警告: 指针减法只适用于指向同一数组的两个指针。如果两个指针指向不同的数组,结果是未定义的。我曾经见过有人拿两个不同数组的指针做减法,结果跑出来的值完全没意义,调试了半天才发现问题。

指针与多维数组

二维数组的指针访问稍微复杂一点。先看定义:

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

matrix 的类型是 int (*)[4],也就是指向一个包含 4 个 int 的数组的指针。所以:

  • matrix 指向第 0 行
  • matrix + 1 指向第 1 行
  • *(matrix + 1) 是第 1 行的首地址,等价于 matrix[1]
  • *(matrix + 1) + 2 是第 1 行第 2 列的地址
  • *(*(matrix + 1) + 2) 是第 1 行第 2 列的值,即 7

说白了,二维数组的指针访问就是两层解引用。第一层拿到行地址,第二层拿到元素值。

用代码验证一下:

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

int (*p)[4] = matrix;   // 指向二维数组的指针

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

输出结果就是整个矩阵。你看,用指针访问多维数组,代码虽然看起来复杂,但效率很高,而且能让你更深刻地理解内存布局。

知识体系图

下面这张图帮你理清指针与数组的核心关系:

指针与数组核心关系图 数组名 arr 指针变量 p 等价于 &arr[0] 数组名特性 • 指针常量,不可修改 • 不能自增/自减 • 不能赋值给其他数组名 指针变量特性 • 指针变量,可修改 • 可以自增/自减 • 可以指向其他数组 访问数组元素的两种方式 下标法 arr[i] 或 p[i] 指针偏移法 *(arr + i) 或 *(p + i) 指针算术运算 p++、p--、p + n

避坑指南

最后,分享几个我踩过的坑:

  • 不要对数组名做自增操作arr++ 是编译错误,因为数组名是常量。如果你需要遍历,用指针变量。
  • 指针越界不会报错:C语言不会检查指针是否越界。我曾经在项目中写了个 p += 100,结果把别的变量给改了,查了两天才找到原因。
  • 指针减法结果是有符号的:两个指针相减得到 ptrdiff_t 类型,是有符号整数。如果指针顺序反了,结果是负数。
  • void* 指针不能做算术运算:因为编译器不知道它指向什么类型,不知道步长是多少。需要先转换成具体类型的指针。

我的建议: 刚开始学指针和数组时,多画内存布局图。把每个变量的地址、值、类型都标出来,慢慢就理解了。我在带新人时,总是让他们先画图再写代码,效果非常好。

好了,这一章的内容就到这里。指针与数组的关系,说白了就是数组名是常量指针,指针变量可以模拟数组访问。理解了这一点,后面的字符串操作、动态内存分配都会轻松很多。


专注资料整理