14、数组与指针的等价性:下标运算的本质、指针与数组的互换使用、多维数组的指针等价形式

好,咱们今天聊一个C语言里特别有意思的话题——数组和指针到底是不是一回事?

很多初学者会问:「数组名不就是指针吗?」嗯,这个问题我当年也纠结过。后来在嵌入式项目里踩过坑,才真正搞明白。今天我把这些经验掰开揉碎了讲给你听。

14.1 下标运算的本质:编译器在背后做了什么?

先看一个最基础的问题:a[i] 到底是什么意思?

你写 a[3],编译器其实把它翻译成了 *(a + 3)。没错,下标运算符 [] 的本质就是指针运算的语法糖。

核心公式:

a[i] == *(a + i)

甚至更夸张:i[a] == *(i + a) 也是合法的!

我第一次看到 3[a] 这种写法时,觉得这肯定是语法错误。结果编译通过,运行正常。为什么?因为 [] 就是个加法再解引用,加法是可交换的。

#include <stdio.h>

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    
    // 这三种写法完全等价
    printf("%d\n", arr[2]);      // 30
    printf("%d\n", *(arr + 2));  // 30
    printf("%d\n", 2[arr]);      // 30 —— 别这么写,但要知道它合法
    
    return 0;
}

我的建议:虽然 i[a] 合法,但千万别在生产代码里这么写。代码是写给人看的,不是写给编译器看的。可读性永远第一。

14.2 指针与数组的互换使用:什么时候可以?什么时候不行?

很多人以为「数组名就是指针」,其实只说对了一半。

数组名在大多数表达式中会「退化」成指向首元素的指针。但有三个例外:

  1. sizeof 运算符sizeof(arr) 返回整个数组的大小,不是指针大小
  2. & 取地址运算符&arr 的类型是 int (*)[5],指向整个数组
  3. 字符串字面量初始化字符数组char s[] = "hello"; 这里的数组是真正的数组
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;  // arr 退化为指针
    
    printf("sizeof(arr) = %zu\n", sizeof(arr));  // 20 (5 * 4)
    printf("sizeof(p)  = %zu\n", sizeof(p));     // 8 (64位系统)
    
    // 但下标操作完全一样
    printf("arr[2] = %d, p[2] = %d\n", arr[2], p[2]);  // 都是 3
    
    return 0;
}

我曾经踩过的坑:在函数参数里写 void func(int arr[]),然后以为 sizeof(arr) 能拿到数组长度。结果呢?arr[] 作为函数参数时,编译器自动把它当作指针处理。sizeof(arr) 返回的是指针大小,不是数组大小。

所以,传递数组时一定要同时传长度参数,或者用 sizeof(arr)/sizeof(arr[0])原作用域里计算。

14.3 多维数组的指针等价形式

二维数组的指针等价性,是很多人绕不过去的坎。咱们一步步拆开看。

假设有:

int matrix[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

matrix 的类型是 int [3][4],退化后是 int (*)[4]——指向「包含4个int的数组」的指针。

所以:

  • matrix 等价于 &matrix[0],类型 int (*)[4]
  • matrix[i] 等价于 *(matrix + i),类型 int [4],退化后是 int *
  • matrix[i][j] 等价于 *(*(matrix + i) + j)
#include <stdio.h>

int main() {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };
    
    // 访问第2行第3列的元素
    printf("%d\n", matrix[1][2]);          // 7
    printf("%d\n", *(*(matrix + 1) + 2));  // 7
    printf("%d\n", *(matrix[1] + 2));      // 7
    
    // 用指针遍历二维数组
    int *p = &matrix[0][0];
    for(int i = 0; i < 12; i++) {
        printf("%d ", p[i]);
    }
    // 输出:1 2 3 4 5 6 7 8 9 10 11 12
    
    return 0;
}

关键理解:

二维数组在内存中是连续线性存储的。行优先排列。所以你可以用一级指针遍历整个二维数组——前提是你知道总元素个数。

14.4 多维数组作为函数参数

写函数处理二维数组时,很多人会卡在参数声明上。

// 正确写法:必须指定第二维大小
void print_matrix(int mat[][4], int rows) {
    for(int i = 0; i < rows; i++) {
        for(int j = 0; j < 4; j++) {
            printf("%d ", mat[i][j]);
        }
        printf("\n");
    }
}

// 等价写法:用数组指针
void print_matrix2(int (*mat)[4], int rows) {
    // 用法完全一样
}

为什么必须指定第二维?因为编译器需要知道每行有多少个元素,才能计算 mat[i][j] 的偏移量。

我个人的习惯:处理多维数组时,如果维度固定,我会用 typedef 定义类型别名,代码会清晰很多。

typedef int Row[4];  // Row 是包含4个int的数组类型
void print_matrix(Row mat[], int rows);

14.5 知识体系总览

下面这张图帮你理清数组与指针等价性的核心脉络:

数组与指针等价性 下标运算本质 a[i] == *(a + i) i[a] 也合法(不推荐) [] 是加法+解引用的语法糖 指针与数组互换使用 数组名大多数情况退化为指针 三个例外:sizeof、&、初始化 函数参数中 [] 等价于 * 多维数组指针等价 matrix[i][j] == *(*(matrix+i)+j) matrix 类型:int (*)[4] 内存连续线性存储 核心:理解编译器如何计算偏移量,比死记硬背公式更重要

14.6 实战中的避坑指南

最后,分享几个我实际项目中遇到的教训:

  • 别对数组名用 ++:数组名不是左值,不能自增。但指针可以。
  • 二维数组传参时,第二维必须常量:C99 支持变长数组(VLA),但可移植性差,慎用。
  • 用指针遍历二维数组时,注意边界:我曾经写了个 p[i][j] 的嵌套循环,结果 p 是一级指针,编译直接报错——类型不匹配。

我曾经犯过的错:在一个通信协议解析模块里,我把接收缓冲区定义成 uint8_t buf[256],然后在另一个文件里用 extern uint8_t *buf 声明。结果 sizeof 行为完全不一样,导致缓冲区溢出。血的教训——extern 声明要和定义保持一致

嗯,数组和指针的等价性,说白了就是理解编译器怎么算地址。你把这个搞透了,后面看任何复杂的指针声明都不会怵。


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