不知道你有没有被这样的问题逼疯过:同样写下 int *p[3] 和 int (*p)[3],一字之差,一个是“数组”,一个是“指针”;一个先跟下标结合,一个先跟星号结合。我在前几年带新人时发现,十个里面有八个会把这两个概念背反,哪怕是写过两年代码的人,也时常在函数传参、动态二维数组、字符串列表这些场景里被按在地上摩擦。这篇东西我就把这些年踩过的坑、看过的核心逻辑一起整理出来,不搞那种看三遍还半懂不懂的背诵口诀,而是从类型、内存、编译器的角度把这两个概念彻底撕开。
这篇文章适合刚学完指针基础、对“数组名和指针”开始怀疑人生的读者,也适合工作了一两年看到这两个词依然要先翻资料的开发者。看完你至少能解决三个核心问题:第一,看到复杂的指针声明能靠规则拆明白;第二,遇到二维数组、字符串列表这些场景,知道该用数组指针还是指针数组;第三,面试被问到这两个概念时,能把“为什么”说得清清楚楚,而不是只背出结论。
1. 两个概念,越学越浑?先看声明在“骗”你什么
1.1 优先级才是罪魁祸首
C 语言里有一类知识点,不是难在算法,而是难在语法本身。“数组指针”和“指针数组”这一对就是最典型的例子。背后的根本原因是操作符优先级:[] 下标操作符的优先级高于 * 解引用操作符。就这么一条规则,导致同一个变量名和同一个类型符号组合在一起,能产生完全不同的含义。
先看 int *p[3]。因为 [] 优先级高于 *,所以 p 先和 [3] 结合,说明 p 本身是一个容量为 3 的数组。既然 p 是数组,数组元素是什么呢?元素类型由剩下的 int * 决定,也就是“指向整型的指针”。所以这句话翻译成人话就是:p 是一个数组,里面装了 3 个指向 int 类型的指针。
再看 int (*p)[3]。括号在 C 声明里永远是最强优先级,它强行让 p 先和 * 结合,于是 p 首先是一个指针。指向的目标是什么呢?越过右边的 [3] 可以知道,这个指针指向一个包含 3 个 int 元素的数组。所以这句话翻译成人话就是:p 是一个指针,它指向一个“能装 3 个 int 的数组”。
这两者从内存布局到用处都完全不同,唯一的共同点就是写法长得像,造成的后果就是初学者把这两张脸反复搞混。我在很多代码评审里见过有人写下 int (*p)[3],心里想的却是“三个指针变量”,然后往里面塞 &a、&b、&c 这种地址,编译之后报了一堆错还不知道为什么。只有先意识到“优先级是罪魁祸首”,你才能在声明这一关把概念掰正过来。
1.2 用一个“右左法则”把声明读明白
遇到那种嵌套了括号、数组、指针的复杂声明,与其凭记忆硬猜,不如掌握一套通用的阅读方法。这个方法在很多经典教材里被称为“右左法则”,我在这里用最简单的方式讲一遍。
从标识符 p 开始向右看,如果遇到右括号 ) 或者 ],就先改变方向,回头往左看括号内的内容;得出一个结论之后,再跳出这层括号继续向右看,如此反复,直到整个声明读完。以 int (*p)[3] 为例:先从 p 开始向右看,发现右边是 ),那就把注意力转到括号左侧,看到 *,于是得到第一条结论“p 是指针”。接着继续往右看,此时已经跳出内层括号,右侧是 [3],说明指针指向的对象是一个长度为 3 的数组。最后再看这些数组元素的类型,左端写着 int,于是整条声明宣告破译:p 是指向“具有 3 个 int 的数组”的指针。
这个方法不需要你背表的顺序,只需要记住两条原则:变量名先和最近的符号结合;[] 和 * 同时出现时,谁离变量名近谁先结合。用这个方法拆 int *p[3] 也一样,“p 先遇到 [3],说明 p 是数组,元素是 int*”。“右左法则”对后面读函数指针、指针数组套指针这类更复杂的声明同样有效,今天这两个概念只是它的入门演练。
1.3 先在心里建立一张对比表
为了能把这两个概念彻底区分开,我整理了一张对比表。这张表我建议你保留下来,每次写代码不确定的时候拿出来看一眼。
| 声明写法 | 真实类型 | 含义 | 常见误解 |
|---|---|---|---|
int *p[3] |
int*[3] |
p 是数组,元素是 3 个 int* |
误认为“指向数组的指针” |
int (*p)[3] |
int(*)[3] |
p 是指针,指向含 3 个 int 的数组 | 误认为“装着指针的数组” |
int p[3][4] |
int[3][4] |
p 是二维数组,每个元素是 int[4] |
误当成 int** 使用 |
int **p |
int** |
p 是指针的指针 | 误当成二维数组名使用 |
很多人会在这里提出一个问题:既然 C 语言里数组名会被“退化”成指针,那 int p[3][4] 和 `int
