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 + 5,arr + 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++是编译错误,因为数组名是常量。如果你需要遍历,用指针变量。 - 指针越界不会报错:C语言不会检查指针是否越界。我曾经在项目中写了个
p += 100,结果把别的变量给改了,查了两天才找到原因。 - 指针减法结果是有符号的:两个指针相减得到
ptrdiff_t类型,是有符号整数。如果指针顺序反了,结果是负数。 - void* 指针不能做算术运算:因为编译器不知道它指向什么类型,不知道步长是多少。需要先转换成具体类型的指针。
我的建议: 刚开始学指针和数组时,多画内存布局图。把每个变量的地址、值、类型都标出来,慢慢就理解了。我在带新人时,总是让他们先画图再写代码,效果非常好。
好了,这一章的内容就到这里。指针与数组的关系,说白了就是数组名是常量指针,指针变量可以模拟数组访问。理解了这一点,后面的字符串操作、动态内存分配都会轻松很多。