数组与指针的纠缠:数组名与指针的关系
说实话,数组和指针的关系,是C语言里最容易让人头晕的地方之一。我当年刚学的时候,也在这上面栽过跟头。明明感觉是两个东西,怎么用着用着就分不清了?
今天咱们就把这层窗户纸捅破。你想想看,数组名到底是不是指针?答案是:是,也不是。
数组名是什么?
数组名代表的是整个数组的首地址。但它不是普通的指针变量——它是一个常量指针。什么意思?就是你不能修改它的值。
核心结论:数组名是数组首元素的地址,类型是 指向数组元素的指针常量。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 等价于 int *p = &arr[0];
// arr 和 &arr[0] 的值相同
printf("arr = %p\n", arr); // 输出:0x7ff...
printf("&arr[0] = %p\n", &arr[0]); // 输出:0x7ff...
// 但 arr 不能自增
// arr++; // 编译错误!数组名不是左值
我在项目中遇到过有人试图用 arr++ 来遍历数组,结果编译报错后一脸懵。嗯,记住:数组名不能做自增运算,但指针可以。
指针算术运算
指针的加减运算,不是简单的地址加减。它是以指向的数据类型大小为步长的。说白了,p + 1 跳过的不是一个字节,而是整个元素的大小。
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(int占4字节)
printf("p + 2 = %p\n", p + 2); // 输出:0x1008
为什么会这样?因为编译器知道 p 是 int* 类型,所以 p + n 实际地址是 p + n * sizeof(int)。
小技巧:我个人习惯用 p[i] 和 *(p + i) 互相转换来理解指针运算。它们完全等价。
通过指针操作数组元素
既然指针可以指向数组元素,那我们就能用指针来读写数组。这比用下标访问更灵活,尤其是在处理复杂数据结构时。
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 方式一:下标法
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
// 方式二:指针法
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 方式三:移动指针
for (int *q = arr; q < arr + 5; q++) {
printf("%d ", *q);
}
三种方式输出结果一样。但第三种方式有个好处——你不需要保留原始指针位置。不过要注意,移动后的指针就找不回原点了,所以记得备份。
避坑指南:我曾经在写一个字符串处理函数时,直接移动了传入的指针参数,结果函数返回后主调函数再也拿不到原始地址了。从那以后,我养成了习惯:永远用临时指针遍历数组,不要动原始指针。
数组名 vs 指针变量:一张表说清楚
| 特性 | 数组名 | 指针变量 |
|---|---|---|
| 是否可修改 | 否(常量) | 是(变量) |
| 占用空间 | 整个数组大小 | 指针本身大小(4或8字节) |
| 取地址结果 | 指向整个数组的指针 | 指向指针变量的指针 |
| sizeof 结果 | 数组总字节数 | 指针本身大小 |
| 能否自增 | 不能 | 能 |
你看,区别还是挺明显的。但为什么很多人觉得它们一样?因为在大多数表达式中,数组名会隐式转换为指向首元素的指针。这就是所谓的"数组退化"。
数组退化的陷阱
当数组作为函数参数传递时,它会退化为指针。这意味着你丢失了数组的长度信息。
void print_array(int arr[]) {
// 这里的 arr 实际上是个指针!
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出:8(64位系统)
}
int main() {
int arr[10] = {0};
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 输出:40(10 * 4)
print_array(arr);
return 0;
}
我见过不少新手在函数里用 sizeof(arr) / sizeof(arr[0]) 来算数组长度,结果发现永远都是1或2。原因就在这里——数组已经退化成指针了。
最佳实践:我建议在传递数组时,总是同时传递数组长度。要么用两个参数,要么用结构体封装。
指针与多维数组
二维数组的指针操作稍微复杂一点。记住一个原则:逐层解引用。
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// matrix 的类型是 int (*)[4],指向包含4个int的数组
int (*p)[4] = matrix;
// 访问 matrix[1][2]
printf("%d\n", *(*(p + 1) + 2)); // 输出:7
// 等价于 p[1][2]
这里 p + 1 跳过了整整一行(4个int),*(p + 1) 得到第二行的首地址,再加2再解引用就是第二行第三列的元素。
知识体系总览
下面这张图帮你理清本章的核心逻辑:
总结一下
数组名和指针,就像一对双胞胎——长得像,但性格不同。数组名是常量,指针是变量。数组名在大多数场合会"伪装"成指针,但别被它骗了。
我个人习惯在写代码时,能用下标就用下标,可读性更好。只有在需要高效遍历、或者处理动态内存时,才用指针操作。你想想看,代码首先是给人读的,其次才是给机器跑的。
嗯,数组与指针的纠缠,今天就聊到这儿。记住那张表,记住数组退化的陷阱,你就能少踩很多坑。