第四章:指针与一维数组——数组名作为指针常量、通过指针遍历数组、指针数组与数组指针的区别

数组和指针,在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) ⇔ *p++ 指针数组 vs 数组指针 int *p[5] 与 int (*p)[5] 不能 arr++ &arr[0] 等价 下标法 指针偏移 移动指针 指针数组 装指针的数组 数组指针 指向数组的指针 核心:理解声明时的结合顺序 [] 优先级高于 *,括号改变结合顺序

这张图把本章的三个核心知识点串起来了。你从左到右看:数组名是指针常量,不能改;通过指针遍历数组有三种等价写法;指针数组和数组指针的区别在于谁先跟变量名结合。

说实话,这些概念刚接触时确实容易晕。但只要你记住一个原则——看声明时,先看变量名跟谁结合——就能分清大部分情况。我在带新人时,经常让他们在纸上画结合顺序图,画几次就通了。

本章要点速记:

  • 数组名是常量指针,不能自增或赋值
  • arr[i] 等价于 *(arr + i)
  • int *p[5] 是指针数组(p是数组)
  • int (*p)[5] 是数组指针(p是指针)
  • 二维数组名是数组指针类型,不是二级指针

公众号:蓝海资料掘金营,微信 deep3321