第19章:指针与数组——指针访问数组元素,数组名作为指针常量,指针运算

指针和数组,在C语言里就像一对双胞胎。长得像,性格却不同。我做了这么多年嵌入式开发,见过太多人在它们身上栽跟头。今天咱们就把这层窗户纸捅破。

19.1 数组名:披着数组外衣的指针常量

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

很多人背过「数组名就是首地址」。对,但不全对。准确地说,数组名是一个指针常量——它的值就是数组首元素的地址,而且这个值不能改。

核心区别:

  • arr 等价于 &arr[0],类型是 int*
  • arr 是常量,不能做 arr++arr = something
  • 而一个 int* p 指针变量,可以随便指向别处

我在项目中遇到过这样一个bug:有人写了 arr++ 想遍历数组,编译器直接报错。他一脸懵:「指针不就能++吗?」嗯,数组名不是普通指针,它是常量。

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;      // 正确:p指向arr[0]
// arr = p;        // 错误:arr是常量,不能赋值
// arr++;          // 错误:不能修改数组名
p++;               // 正确:p是变量,可以移动

19.2 指针访问数组元素:三种写法,一个意思

访问数组第 i 个元素,C语言给了你三种姿势。我个人习惯用下标法,因为可读性最好。但你要知道,它们本质是一样的。

写法 含义 备注
arr[i] 下标法 最直观,推荐
*(arr + i) 指针法 底层本质
p[i] 指针当下标 p指向数组首元素时可用
int arr[4] = {1, 2, 3, 4};
int *p = arr;

// 三种写法,完全等价
printf("%d\n", arr[2]);      // 3
printf("%d\n", *(arr + 2));  // 3
printf("%d\n", p[2]);        // 3

小技巧: 编译器看到 arr[i] 时,会自动翻译成 *(arr + i)。所以下标操作本质上就是指针运算。你想想看,2[arr] 这种写法居然也能编译通过——因为 2[arr] 等价于 *(2 + arr)。虽然没人这么写,但能帮你理解本质。

19.3 指针运算:加减的学问

指针加减整数,不是简单的地址加减。它是以「元素大小」为步长的。说白了,p + 1 跳过一个元素,而不是跳过一个字节。

int arr[3] = {100, 200, 300};
int *p = arr;

printf("p   = %p\n", p);      // 假设 0x1000
printf("p+1 = %p\n", p + 1);  // 0x1004(int占4字节)
printf("p+2 = %p\n", p + 2);  // 0x1008

为什么会这样?因为指针的类型决定了步长。int* 每次加4字节,char* 每次加1字节,double* 每次加8字节。我在调试一个通信协议时,就因为这个特性踩过坑——用 char* 遍历结构体数组,步长算错了,数据全对不上。

避坑指南: 我曾经在STM32上写DMA传输,用指针遍历一个 uint16_t 数组。结果不小心把指针声明成了 uint8_t*,每次加1只移动了1字节,数组后半段数据全读错了。排查了整整一下午。记住:指针的类型决定了它移动的步长

19.4 指针相减:得到的是元素个数

两个同类型的指针相减,结果不是地址差,而是元素个数。这个特性在计算数组长度时特别有用。

int arr[8] = {0};
int *start = &arr[2];
int *end   = &arr[6];

int count = end - start;  // 结果是4,不是16
printf("元素个数: %d\n", count);  // 4

嗯,这里要注意:指针相减要求两个指针指向同一个数组(或者同一个对象之后的下一个位置),否则结果是未定义的。别拿两个不同数组的指针去减,那是自找麻烦。

19.5 数组名 vs &数组名:一个经典陷阱

这个问题我面试时经常问,能答对的人不多。

int arr[5] = {0};
printf("arr     = %p\n", arr);      // 首元素地址
printf("&arr[0] = %p\n", &arr[0]);  // 同上
printf("&arr    = %p\n", &arr);     // 整个数组的地址

// 数值上相同,但类型不同
printf("arr+1   = %p\n", arr + 1);   // 跳过一个int
printf("&arr+1  = %p\n", &arr + 1);  // 跳过整个数组(5个int)

arr 的类型是 int*,指向一个int元素。&arr 的类型是 int (*)[5],指向一个包含5个int的数组。虽然它们打印出来的地址值一样,但加1之后,跨度完全不同。

记住这个规律:

  • arr + 1 → 移动 sizeof(int) 字节
  • &arr + 1 → 移动 sizeof(arr) 字节,即 5 * sizeof(int)

19.6 知识体系图

下面这张图帮你理清本章的核心逻辑。建议保存下来,写代码时对照着看。

指针与数组 数组名 指针常量,不可修改 访问元素 arr[i] / *(arr+i) / p[i] 指针运算 加减以元素大小为步长 指针相减 得到元素个数 &数组名 类型:int(*)[n] 常见陷阱 类型不匹配 / 步长错误 核心:数组名是指针常量,指针运算以元素大小为步长 理解类型 = 避免80%的指针错误

19.7 实战:用指针遍历二维数组

二维数组在内存中是连续存储的。你可以用一个一级指针从头到尾遍历所有元素。我在做图像处理时经常这么干——把二维像素数组当一维处理,效率更高。

int matrix[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

int *p = &matrix[0][0];  // 或者直接用 matrix[0]
for (int i = 0; i < 6; i++) {
    printf("%d ", *(p + i));  // 1 2 3 4 5 6
}

个人习惯: 遍历二维数组时,我更喜欢用 int *p = &matrix[0][0] 这种写法,虽然啰嗦一点,但意图明确。你想想看,如果写成 int *p = matrix,编译器会报警告——因为 matrix 的类型是 int (*)[3],不是 int*。虽然强制转换也能用,但写清楚更安全。

19.8 本章小结

  • 数组名是指针常量,不能自增自减,也不能被赋值
  • 访问元素有下标法和指针法,本质相同
  • 指针加减以元素大小为步长,类型决定步长
  • 指针相减得到元素个数,不是字节数
  • &数组名得到整个数组的地址,类型是数组指针

指针和数组这块,说白了就是「类型决定行为」。搞懂了类型,你就搞懂了一切。我当年刚学C时,也是被这些绕得晕头转向,后来在项目里踩了几次坑,才真正理解。别急,多写多练,自然就通了。


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