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*

读法技巧:从变量名开始,先看右边,再看左边。遇到括号先处理。

知识体系图

下面这张图帮你理清本章的核心脉络:

指针与数组 知识体系 数组与指针 数组名即首地址 地址常量,不可修改 指针访问数组元素 arr[i] ⇔ *(arr + i) 指针 vs 数组名 变量 vs 常量 数组指针 int (*p)[N] 二维数组传参 矩阵运算 核心:理解数组的「地址常量」本质,区分指针与数组名

总结一下:数组名是地址常量,指针是地址变量。数组名在大多数表达式中退化为指针,但 sizeof& 操作时保持数组类型。数组指针 int (*p)[N] 是处理二维数组的利器。

嗯,这些概念刚开始确实有点绕。但你在代码里多写几遍,多踩几次坑,自然就记住了。我当年也是被 arr++ 的编译错误教育过,才真正理解了数组名和指针的区别。


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