第四章:指针与一维数组——数组名作为指针常量、通过指针遍历数组、指针数组与数组指针的区别
数组和指针,在C语言里就像一对双胞胎。长得像,性格却不同。很多初学者在这里栽跟头,我当年也不例外。记得我第一次用指针遍历数组时,还闹了个段错误,差点把开发板搞死机。嗯,今天咱们就把这层窗户纸捅破。
4.1 数组名:披着数组外衣的指针常量
先问一个问题:int arr[5] 里的 arr 到底是什么?
很多人脱口而出:「数组名就是首地址。」对,但不全对。准确地说,数组名是一个指针常量。它指向数组的第一个元素,但你不能修改它。
核心结论:
arr等价于&arr[0]arr的类型是int * const(指针本身不可修改)- 不能做
arr++或arr = something
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
printf("arr = %p\n", arr);
printf("&arr[0] = %p\n", &arr[0]);
// 这两个地址一模一样
// arr++; // 编译错误!arr 是常量
// arr = NULL; // 同样不行
return 0;
}
为什么会这样?因为数组名在编译时就固定了。它不像普通指针变量那样可以指向别处。我个人习惯把数组名理解为「一块内存区域的身份证」——你可以用它访问内存,但不能把它换成别人的身份证。
避坑指南:我曾经在项目里写过 arr++ 来移动数组指针,结果编译器直接报错。后来改用 int *p = arr; p++; 才搞定。记住:数组名不能自增,但你可以用另一个指针变量来代劳。
4.2 通过指针遍历数组:两种写法,一个道理
遍历数组,最直观的方式是用下标:arr[i]。但用指针怎么写?其实 arr[i] 本质上就是 *(arr + i)。编译器在背后帮你做了指针运算。
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向 arr[0]
// 方式一:下标法
for (int i = 0; i < 5; i++) {
printf("arr[%d] = %d\n", i, arr[i]);
}
// 方式二:指针法
for (int i = 0; i < 5; i++) {
printf("*(p + %d) = %d\n", i, *(p + i));
}
// 方式三:移动指针
for (int *q = arr; q < arr + 5; q++) {
printf("%d ", *q);
}
printf("\n");
return 0;
}
三种方式,结果一样。但性能上有细微差别吗?说实话,现代编译器优化后基本没区别。我建议你根据场景选:
- 需要随机访问(比如 arr[100]):用下标,更直观
- 顺序遍历:用指针移动,代码更紧凑
- 嵌入式底层操作:用指针,因为有时候你要操作的是寄存器地址,不是数组
注意:指针移动时一定要保证不越界。我曾经在调试一个通信协议时,因为指针多移了一位,把相邻的内存数据给改了,查了整整两天。嗯,从那以后我写指针遍历都会加上边界检查。
4.3 指针数组与数组指针:名字只差一个字,含义天差地别
这两个概念,我敢说有一半的C程序员一开始都搞混过。包括我。咱们先看定义:
| 概念 | 定义写法 | 本质 | 读法口诀 |
|---|---|---|---|
| 指针数组 | int *p[5] |
一个数组,元素都是指针 | 「p 先跟 [5] 结合,是个数组」 |
| 数组指针 | int (*p)[5] |
一个指针,指向一个数组 | 「p 先跟 * 结合,是个指针」 |
怎么记?看运算符优先级:[] 的优先级高于 *。所以:
int *p[5]:p 先跟 [5] 结合,说明 p 是数组,元素类型是int *int (*p)[5]:括号让 p 先跟 * 结合,说明 p 是指针,指向一个长度为5的 int 数组
说白了,就是看谁先跟 p「结婚」。谁先结合,p 就是谁的类型。
4.3.1 指针数组:装指针的数组
指针数组最常见的用途是处理字符串数组。比如:
#include <stdio.h>
int main() {
// 指针数组:每个元素是一个 char* 指针
char *names[3] = {
"Alice",
"Bob",
"Charlie"
};
for (int i = 0; i < 3; i++) {
printf("names[%d] = %s\n", i, names[i]);
}
return 0;
}
这里 names 是一个数组,里面存了三个指针。每个指针指向一个字符串常量。这样做的好处是:字符串长度可以不同,而且修改指针比复制整个字符串快得多。
个人经验:我在做菜单系统时,用指针数组来管理多个选项文本。想切换语言?只需要换一组指针数组就行,不用动字符串本身。非常灵活。
4.3.2 数组指针:指向整个数组的指针
数组指针用得相对少一些,但在二维数组传参时特别有用。看例子:
#include <stdio.h>
int main() {
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 数组指针:指向一个长度为3的 int 数组
int (*p)[3] = arr; // arr 是 int (*)[3] 类型
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", p[i][j]); // 可以像二维数组一样用
}
printf("\n");
}
return 0;
}
注意:arr 在这里是二维数组的数组名,它的类型是 int (*)[3],而不是 int **。很多人搞混这一点。你想想看,如果写成 int **p = arr;,编译器会报警告,因为类型不匹配。
避坑指南:我曾经在写一个图像处理函数时,想用 int ** 接收二维数组,结果传参后数据全乱了。后来改成 int (*p)[width] 才正常。记住:二维数组名不是二级指针,它是数组指针。
4.4 知识体系总览
下面这张图帮你理清本章的核心逻辑:
这张图把本章的三个核心知识点串起来了。你从左到右看:数组名是指针常量,不能改;通过指针遍历数组有三种等价写法;指针数组和数组指针的区别在于谁先跟变量名结合。
说实话,这些概念刚接触时确实容易晕。但只要你记住一个原则——看声明时,先看变量名跟谁结合——就能分清大部分情况。我在带新人时,经常让他们在纸上画结合顺序图,画几次就通了。
本章要点速记:
- 数组名是常量指针,不能自增或赋值
arr[i]等价于*(arr + i)int *p[5]是指针数组(p是数组)int (*p)[5]是数组指针(p是指针)- 二维数组名是数组指针类型,不是二级指针
公众号:蓝海资料掘金营,微信 deep3321