数组与指针的纠缠:数组名与指针的关系

说实话,数组和指针的关系,是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再解引用就是第二行第三列的元素。

知识体系总览

下面这张图帮你理清本章的核心逻辑:

数组与指针核心知识体系 数组名 vs 指针 数组名特性 • 常量指针,不可修改 • sizeof 返回整个数组大小 • 取地址得数组指针 指针变量特性 • 变量,可修改指向 • sizeof 返回指针大小 • 可自增自减 指针算术运算 • p + n 跳 n * sizeof(类型) 字节 • 支持 ++、--、+、- 运算 数组退化陷阱 • 传参时退化为指针,丢失长度

总结一下

数组名和指针,就像一对双胞胎——长得像,但性格不同。数组名是常量,指针是变量。数组名在大多数场合会"伪装"成指针,但别被它骗了。

我个人习惯在写代码时,能用下标就用下标,可读性更好。只有在需要高效遍历、或者处理动态内存时,才用指针操作。你想想看,代码首先是给人读的,其次才是给机器跑的。

嗯,数组与指针的纠缠,今天就聊到这儿。记住那张表,记住数组退化的陷阱,你就能少踩很多坑。