4. 指针与数组:数组名即首地址、通过指针访问数组元素、指针与数组名的区别、数组指针
数组和指针,这俩在C语言里就像一对双胞胎。长得像,但性格完全不同。很多初学者在这上面栽跟头,我当年也不例外。
记得我第一次用指针操作数组时,写了个arr++,编译器直接报错。我当时还纳闷:数组名不就是地址吗?地址为什么不能自增?
嗯,这个问题问得好。咱们今天就把它彻底讲透。
4.1 数组名即首地址
先看一个最基础的事实:数组名代表数组首元素的地址。
int arr[5] = {10, 20, 30, 40, 50};
printf("%p\n", arr); // 输出数组首地址
printf("%p\n", &arr[0]); // 输出第一个元素的地址
// 这两个值是一样的
运行结果会告诉你,arr和&arr[0]打印出来的地址完全相同。这就是所谓的「数组名即首地址」。
但这里有个坑——数组名不是指针变量,它是地址常量。常量意味着你不能修改它。
核心理解:
arr的类型是int[5],不是int*- 在大多数表达式中,
arr会「退化」为指向首元素的指针 - 但
arr本身不是变量,不能赋值
4.2 通过指针访问数组元素
既然数组名可以当作指针用,那用指针操作数组就顺理成章了。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p指向数组首元素
// 方式一:下标法
printf("%d ", arr[2]); // 30
// 方式二:指针偏移法
printf("%d ", *(p + 2)); // 30
// 方式三:指针自增遍历
for (int i = 0; i < 5; i++) {
printf("%d ", *p);
p++; // p指向下一个元素
}
这三种方式,本质上是一样的。arr[i] 在编译器眼里就是 *(arr + i)。你想想看,下标操作符其实就是在做指针运算。
我在项目中遇到过一种情况:用指针遍历数组比用下标快。其实在现代编译器优化下,两者性能几乎没差别。但有些底层驱动代码里,我仍然习惯用指针——因为更直观地表达了「我在移动位置」这个动作。
小技巧:
用指针遍历数组时,可以这样写:
int *p = arr;
int *end = arr + 5; // 指向数组末尾的下一个位置
while (p < end) {
printf("%d ", *p++);
}
这种写法避免了在循环里计算 arr[i] 的偏移量,代码也更简洁。
4.3 指针与数组名的区别
这是最容易混淆的地方。我画个表,你一看就明白。
| 对比项 | 数组名 arr | 指针变量 p |
|---|---|---|
| 本质 | 地址常量 | 地址变量 |
| 能否自增 | 不能(arr++ 非法) | 可以(p++ 合法) |
| 能否赋值 | 不能(arr = 其他地址 非法) | 可以(p = 其他地址 合法) |
| sizeof 结果 | 整个数组的字节数 | 指针本身的字节数(4或8) |
| & 操作 | &arr 是数组指针(类型 int(*)[5]) | &p 是指针的地址(类型 int**) |
我曾经犯过一个低级错误:在函数里用 sizeof(arr) 想获取数组大小,结果只拿到了指针的大小。因为数组作为函数参数时,已经退化成指针了。
避坑指南:
数组作为函数参数时,会退化为指针。所以下面这个函数是错的:
// 错误写法
void print_array(int arr[]) {
int len = sizeof(arr) / sizeof(arr[0]); // 这里 arr 是指针!
// ...
}
// 正确写法
void print_array(int arr[], int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
}
记住:数组传参,长度必须另传。
4.4 数组指针
数组指针,顾名思义——指向数组的指针。
它的定义方式有点绕:
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr; // p 是一个指向 int[5] 的指针
注意括号不能少。如果写成 int *p[5],那就变成了「指针数组」——一个数组,里面存了5个指针。完全两码事。
数组指针有什么用?最常见的场景是二维数组传参。
void print_matrix(int (*matrix)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
}
int main() {
int data[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
print_matrix(data, 3);
return 0;
}
这里 int (*matrix)[4] 就是一个数组指针,它指向一个包含4个int的数组。二维数组名 data 的类型是 int (*)[4],正好匹配。
我个人习惯在写矩阵运算、图像处理这类代码时,大量使用数组指针。它让代码的语义更清晰——看到 int (*p)[4],就知道 p 指向的是「一行有4个元素」的数组。
数组指针 vs 指针数组:
int (*p)[5]— 数组指针,p 指向一个 int[5] 数组int *p[5]— 指针数组,p 是一个数组,每个元素是 int*
读法技巧:从变量名开始,先看右边,再看左边。遇到括号先处理。
知识体系图
下面这张图帮你理清本章的核心脉络:
总结一下:数组名是地址常量,指针是地址变量。数组名在大多数表达式中退化为指针,但 sizeof 和 & 操作时保持数组类型。数组指针 int (*p)[N] 是处理二维数组的利器。
嗯,这些概念刚开始确实有点绕。但你在代码里多写几遍,多踩几次坑,自然就记住了。我当年也是被 arr++ 的编译错误教育过,才真正理解了数组名和指针的区别。
公众号:蓝海资料掘金营,微信deep3321