3. 指针与数组:数组名的指针含义、通过指针遍历数组、指针与数组名的区别

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

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

3.1 数组名到底是个啥?

先看一段最基础的代码:

int arr[5] = {10, 20, 30, 40, 50};
printf("%p\n", arr);    // 打印数组名
printf("%p\n", &arr[0]); // 打印首元素地址

你猜怎么着?这两个值一模一样

数组名,说白了就是数组首元素的地址。它是一个地址常量,不是变量。这一点非常关键。

核心结论:arr 等价于 &arr[0],类型是 int*(指向 int 的指针)。

但注意,有例外情况。当数组名被 sizeof 操作符包围时,它代表整个数组:

int arr[5] = {10, 20, 30, 40, 50};
printf("%zu\n", sizeof(arr));   // 输出 20(5个int × 4字节)
printf("%zu\n", sizeof(&arr[0])); // 输出 8(指针大小,64位系统)

嗯,这里要注意:sizeof(arr) 返回的是整个数组占用的字节数,而不是指针的大小。这是数组名和指针最明显的区别之一。

3.2 通过指针遍历数组

我个人习惯用指针遍历数组,尤其是在嵌入式开发中。为什么?因为指针操作更底层,效率更高,而且能让你对内存布局有更清晰的认识。

来看一个典型的例子:

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;  // p指向数组首元素

for (int i = 0; i < 5; i++) {
    printf("arr[%d] = %d\n", i, *(p + i));
}

这里 *(p + i) 等价于 p[i],也等价于 arr[i]。你想想看,指针的加减运算其实就是在内存中"跳"字节。

我在项目中遇到过这样一个场景:需要从一个环形缓冲区中读取数据。用指针遍历比用数组下标方便得多:

uint8_t buffer[256];
uint8_t *read_ptr = buffer;
uint8_t *write_ptr = buffer;

// 写入数据
*write_ptr++ = data_byte;
if (write_ptr >= buffer + 256) {
    write_ptr = buffer;  // 绕回
}

// 读取数据
uint8_t value = *read_ptr++;
if (read_ptr >= buffer + 256) {
    read_ptr = buffer;
}

你看,指针的自增和比较操作,让环形缓冲区的实现变得非常自然。

3.3 指针与数组名的区别

很多人觉得数组名就是指针,其实不对。我列个表,一目了然:

特性 数组名 指针变量
本质 地址常量 地址变量
能否赋值 不能(arr = something 是错的) 可以(p = something 是对的)
sizeof 含义 整个数组的大小 指针本身的大小(4或8字节)
取地址操作 &arr 得到整个数组的地址(类型不同) &p 得到指针变量的地址
作为函数参数 退化为指针 就是指针

这里有个坑,我曾经踩过:

我曾经在代码里写过 arr++,想着让数组名"移动"到下一个元素。结果编译器直接报错。数组名是常量,不能自增。你得用指针变量才行。

正确的做法是:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;  // 把数组名赋给指针变量
p++;           // 指针可以自增,现在指向 arr[1]

3.4 二维数组与指针

二维数组的数组名,指向的是第一行的地址。说白了,它是一个"数组的数组"。

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

// matrix 的类型是 int (*)[4],指向一个包含4个int的数组
int (*row)[4] = matrix;  // row指向第一行

for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 4; j++) {
        printf("%2d ", *(*(row + i) + j));
    }
    printf("\n");
}

这里 *(*(row + i) + j) 等价于 matrix[i][j]。你想想看,row + i 跳过了 i 行,*(row + i) 得到第 i 行的首地址,再加 j 就是第 j 个元素。

我在项目中处理图像数据时经常这么用。一张 640×480 的灰度图,用二维数组表示,指针操作比下标快不少。

3.5 避坑指南

最后,分享几个我实际工作中遇到的坑:

  • 数组作为函数参数会退化为指针:在函数内部用 sizeof 得到的是指针大小,不是数组大小。所以通常要额外传一个长度参数。
  • 不要对数组名使用 && 操作:&arr 得到的是整个数组的地址,虽然数值上等于 arr,但类型不同。&arr + 1 会跳过整个数组。
  • 指针加减的单位是元素大小:int* p 加 1,地址增加 4 字节(假设 int 占 4 字节)。不是加 1 个字节。

小技巧:如果你不确定指针加减后指向哪里,可以用 printf 打印地址值来验证。我调试指针相关 bug 时,这招屡试不爽。

3.6 知识结构图

下面这张图总结了本章的核心逻辑,建议你多看几遍:

指针与数组核心知识结构 数组名 本质:地址常量 遍历:指针 + 偏移 区别:常量 vs 变量 sizeof(arr) = 整个数组大小 &arr 得到整个数组地址 *(p + i) 等价于 p[i] 指针加减 = 跳元素 数组名不能自增 函数参数退化为指针 核心:数组名是常量,指针是变量,用法相似但本质不同

好了,关于指针和数组的关系,就讲到这里。记住一句话:数组名是地址常量,指针是地址变量。搞懂这个,后面再学指针数组、数组指针、函数指针什么的,就轻松多了。


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