1. 开篇:为什么“指向数组的指针”总让人一头雾水
我在做C/C++技术答疑时发现一个规律:很多写过一两年代码的开发者,提到二级指针、函数指针都能聊几句,但一碰到“指向数组的指针”就含糊了。尤其是当代码里出现 int (*p)[5] 和 int *p[5] 混在一起的时候,十个人里有七个会愣一下——这俩到底哪个是数组指针,哪个是指针数组?
这个问题的根源其实不在语法本身,而在于我们对“数组名”这个符号的理解不够透彻。数组名到底是地址还是变量?a 和 &a 明明打印出来数值一样,为什么加1的结果不一样?二维数组传参时为什么函数签名那么别扭?这些现象背后其实只有一条主线——指针的算术运算依赖“类型步长”,而数组名的退化规则决定了你拿到的是“谁的地址”。
这篇内容我会从底层地址模型讲起,把指针与数组的关系拆开揉碎,再配上可直接运行的代码示例和调试技巧。无论你是刚学C语言的初学者,还是被C++折磨的进阶者,又或是想系统理清旧知识的开发者,这篇应该都能帮上忙。文中示例以C语言为主,关键的现代C++差异点我也会单独标注。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础概念:数组指针与指针数组的区分
2.1 两个相似到令人抓狂的声明
先看这两行声明:
c复制int *p1[5]; // p1是什么?
int (*p2)[5]; // p2又是什么?
判断方法很笨但很有效:找变量名,然后看它先和谁结合。C语言里 [] 和 () 的优先级高于 *,所以:
p1先和[]结合,说明 p1 是一个数组,数组的元素类型是int *。所以 p1 是“指针数组”——一个存了5个指针的数组。- p2 被括号包住,先和
*结合,说明 p2 是一个指针,它指向的对象是int [5]类型,也就是一个包含5个int的数组。所以 p2 是“数组指针”——一个指向数组的指针。
这两个东西的用途完全不同。指针数组通常用于存放字符串的起始地址,比如命令行参数 char *argv[] 就是典型的指针数组;而数组指针通常用于操作二维数组的行,或者用于函数传参时约束二维数组的列宽。
我在教学时常用一个生活类比:指针数组好比一张通讯录,每一行记录一个人的电话号码,每个人可以各打各的;数组指针则好比一个“指向整本电话簿的指针”,你用它可以找到某一页的起始位置,但这页的结构是固定的。
2.2 数组名退化的机制
这里有个绕不开的概念——数组名退化(array decay)。绝大多数情况下,数组名会隐式转换为指向其首元素的指针。例如:
c复制int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // arr退化为指向arr[0]的指针
这里 p 的类型是 int *,步长4字节(假设32位int),p + 1 指向 arr[1]。
但有三种情况数组名不退化:
- 数组是
sizeof的操作数时:sizeof(arr)得到整个数组占用的字节数,而不是指针的大小。 - 数组作为
&的操作数时:&arr的类型是int (*)[5],指向整个数组。 - 数组是字符串字面量用于初始化字符数组时。
第2点正是理解“指向数组的指针”的钥匙。arr 退化成 int *,但 &arr 是 int (*)[5],两者值相同、类型不同、步长完全不同,所以加1的结果差出“一个数组的长度”。
很多初学者在这里卡住的原因,是把“地址的值”和“地址的语义”混为一谈。地址只是一个数字,但指针的类型决定了这个数字在算术运算中步进多远。这个思路贯穿全文,请务必记牢。
2.3 用sizeof证明类型差异
如果你还对上面两个声明有疑虑,跑一下这段代码就明白了:
c复制#include <stdio.h>
int main(void) {
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr; // 退化的指针
int (*p2)[5] = &arr; // 指向整个数组的指针
printf("sizeof(arr) = %zu\n", sizeof(arr)); // 20
printf("sizeof(p1) = %zu\n", sizeof(p1)); // 8(64位系统)
prin
