3. 指针与数组:数组名的本质

数组和指针,在C语言里就像一对双胞胎。长得像,但性格完全不同。很多初学者在这里栽跟头,说实话,我当年也迷糊过好一阵子。

咱们今天就把这事儿彻底说清楚。

3.1 数组名的本质:首元素地址

先问一个问题:int arr[5] 里的 arr 到底是什么?

很多人脱口而出:「数组名就是首地址。」

嗯,对了一半。更准确地说——数组名是首元素的地址。注意,是「首元素」,不是「数组」本身。

核心结论:

  • arr 等价于 &arr[0]
  • arr + 1 跳过一个元素(而不是跳过整个数组)
  • &arr 才是整个数组的地址(类型是 int (*)[5]

我举个例子你就明白了:

#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]);  // 和上面一样
    printf("&arr     = %p\n", &arr);     // 整个数组的地址
    
    printf("arr + 1  = %p\n", arr + 1);  // 加 4 字节(int)
    printf("&arr + 1 = %p\n", &arr + 1); // 加 20 字节(整个数组)
    
    return 0;
}

你看,arr + 1&arr + 1 的结果完全不同。前者跳过一个 int,后者跳过整个数组。这就是本质区别。

我曾经踩过的坑:

有一次我写一个数组拷贝函数,传参时用了 void func(int arr[])。结果在函数内部用 sizeof(arr) 想算数组长度,死活不对。后来才意识到——数组作为函数参数时,已经退化成指针了。sizeof(arr) 返回的是指针大小,不是数组大小。

3.2 指针访问数组元素

既然数组名就是首元素地址,那用指针访问数组元素就顺理成章了。

两种写法完全等价:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

// 写法一:下标法
printf("%d\n", arr[2]);   // 3
printf("%d\n", p[2]);     // 3

// 写法二:指针偏移法
printf("%d\n", *(arr + 2)); // 3
printf("%d\n", *(p + 2));   // 3

我个人习惯用下标法,可读性更好。但在底层驱动开发中,指针偏移法更常见,因为有时候你需要手动控制地址偏移量。

比如我在做嵌入式项目时,经常要遍历一个缓冲区:

uint8_t buffer[256];
uint8_t *ptr = buffer;

for (int i = 0; i < 256; i++) {
    *(ptr + i) = read_sensor();  // 指针偏移
}

这种写法在汇编层面效率更高,因为编译器可以直接生成基址+偏移的寻址指令。

3.3 指针与多维数组

多维数组,说白了就是「数组的数组」。比如 int matrix[3][4],它包含 3 个元素,每个元素是一个长度为 4 的 int 数组。

matrix 是什么?

matrix 是首元素的地址。首元素是什么?是 matrix[0],它是一个长度为 4 的 int 数组。所以 matrix 的类型是 int (*)[4]——指向长度为 4 的 int 数组的指针。

我画了一张图,帮你理清这个关系:

多维数组内存布局:int matrix[3][4] matrix[0] matrix[1] matrix[2] [0][0] [0][1] [0][2] [0][3] [1][0] [1][1] [1][2] [1][3] [2][0] [2][1] [2][2] [2][3] matrix 地址:0x1000 ~ 0x100F 地址:0x1010 ~ 0x101F 地址:0x1020 ~ 0x102F matrix 指向 matrix[0](整个第一行) matrix + 1 跳过 4 个 int(16 字节) *(matrix + 1) + 2 等价于 matrix[1][2]

你看,matrix 指向第一行,matrix + 1 指向第二行。每一行是一个一维数组,所以 *(matrix + 1) + 2 就是第二行第三个元素的地址,再解引用就是值。

说白了,多维数组的指针操作就是「层层剥开」的过程。

3.4 数组指针 vs 指针数组

这两个概念,名字就差一个字,但完全是两码事。我见过太多人搞混了。

概念 定义 本质 示例
数组指针 指向数组的指针 一个指针,指向一个数组 int (*p)[5]
指针数组 存放指针的数组 一个数组,元素是指针 int *p[5]

怎么记?我有个小技巧:看变量名和谁结合

  • int (*p)[5]*p 先结合,说明 p 是指针,指向一个长度为 5 的 int 数组。这是数组指针
  • int *p[5]p[5] 先结合,说明 p 是数组,元素类型是 int *。这是指针数组

运算符优先级决定了这一切。[] 优先级高于 *,所以 int *p[5] 先被解释为数组,而 int (*p)[5] 用括号强行让 * 先结合。

实用建议:

数组指针常用于二维数组传参。比如:

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");
    }
}

这样传参,函数内部就知道每行有 4 个元素,可以正确计算偏移。

指针数组呢?我常用在字符串处理中。比如:

const char *weekdays[] = {
    "Monday", "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday", "Sunday"
};

for (int i = 0; i < 7; i++) {
    printf("%s\n", weekdays[i]);
}

每个元素是一个 const char *,指向一个字符串常量。这样比二维字符数组灵活得多,字符串长度可以不同。

我曾经犯过的错:

有一次我写 int *p[5] 想定义一个指向数组的指针,结果编译通过但运行结果完全不对。后来才发现我写的是指针数组,里面 5 个指针都没初始化,乱指一气。从那以后,我每次写这类声明都会先默念一遍「运算符优先级」。

小结

这一节的内容,说白了就是三件事:

  • 数组名是首元素地址,不是数组本身的地址。&arr 才是整个数组的地址。
  • 指针访问数组有两种方式:下标法和偏移法,本质一样。
  • 数组指针 vs 指针数组:看括号和优先级,一个是指向数组的指针,一个是存指针的数组。

这些概念在嵌入式开发中天天用。比如操作寄存器缓冲区、处理传感器数据流、解析通信协议帧……说白了,指针和数组的关系搞不清楚,写出来的代码要么跑飞,要么效率低得吓人。

嗯,这一节就到这儿。下一节咱们聊聊指针和函数的关系,那又是一个容易踩坑的地方。


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