在C和C++语言中,指针相关的声明常常让初学者感到困惑,尤其是数组指针和指针数组这两种形式。它们表面上只是括号位置不同,但实际语义截然相反。数组指针指的是一个指针变量,它指向的是一个完整的数组;而指针数组指的是一个数组,其每一个元素都是指针。要准确写出声明并理解已有代码,必须掌握运算符的优先级规则以及系统的阅读方法。

从运算符优先级看声明差异
在C语言声明中,方括号[]的优先级高于星号*。这意味着在没有任何括号干预的情况下,标识符会先与[]结合,从而形成一个数组,剩下的修饰符再作用于数组的元素类型。以int *p[5];为例,由于[]优先,p首先是一个长度为5的数组,然后int *说明数组的每个元素是整型指针。这就是指针数组:它有5个格子,每个格子存的是一个int*。
反过来,如果使用圆括号强制改变结合顺序,例如int (*p)[5];,此时p先与*结合,表明p是一个指针,然后才与[5]结合,表示它指向一个含有5个整数的数组。这种类型就是数组指针,也常被称为行指针,在二维数组传参时非常常见。忽略括号会导致编译器将p解析为数组,从而彻底改变变量性质。
为了更直观地对比,可以用表格列出二者在内存布局和用法上的区别。指针数组本身是一个数组对象,占据连续的空间存放地址;数组指针只是一个指针变量,占用一个指针大小的空间,它指向别处的数组。下列表格总结了关键差异:
| 声明形式 | 类型本质 | 常见用途 |
|---|---|---|
int *p[5]; | 元素为int*的数组 | 存储多个字符串或动态int列表 |
int (*p)[5]; | 指向int[5]的指针 | 二维数组形参、遍历数组行 |
用右左法则解读复杂声明
面对更复杂的声明,例如函数指针数组,单纯靠优先级仍然容易出错。工程上常用“右左法则”来系统化阅读:从标识符开始,先向右看,遇到括号或声明结尾就掉头向左看,再遇到括号再向右,如此往复,同时随时受括号约束。以int (*func[3])(float);为例,func先向右与[3]结合成数组,向左与*结合说明元素是指针,再向右与(float)结合说明是函数指针,最后向左得出返回int。于是这是一个长度为3的数组,元素为指向“参数是float、返回int”的函数的指针。
右左法则在嵌套括号中尤为重要。若写成int *(*p)[5];,先向右p被括号包住,向左见*说明是指针,向右出括号见[5]说明指向长度为5的数组,向左见int *说明数组元素是整型指针。因此这是一个指向“指针数组”的指针,与前面的基础类型形成组合。掌握该方法后,再长的声明也能拆成短句。
下面用一段代码演示如何通过typedef降低复杂度。将复杂声明拆成类型别名,既方便阅读也减少出错:
#include <stdio.h>
// 指针数组类型:长度为5的int*数组
typedef int* IntPtrArray[5];
// 数组指针类型:指向长度为5的int数组
typedef int (*IntArrayPtr)[5];
int main() {
int arr[5] = {1, 2, 3, 4, 5};
IntArrayPtr p = &arr; // p是数组指针,指向整个arr
IntPtrArray q; // q是指针数组,需分别赋值
for (int i = 0; i < 5; i++) {
q[i] = &arr[i];
}
printf("via array ptr: %dn", (*p)[2]);
printf("via ptr array: %dn", *q[2]);
return 0;
}
实际编码中的常见错误与规避
在二维数组作为函数参数传递时,很多人写成void foo(int p[][5]),这其实等价于int (*p)[5],即数组指针。如果误写成int *p[5]作为形参,编译器会将其调整为int **p,此时用p[i][j]访问就会按指针解引用,而不是按二维数组行偏移,导致运行时错误。理解形参中第一维可省略、其余维度必须保留,是写对声明的关键。
另一个易错点是在动态分配时混用二者。若要创建“3行5列”的整型矩阵,正确做法是先分配3个“行指针”还是直接分配一片连续空间并用数组指针接收?使用int (*mat)[5] = malloc(3 * sizeof(int[5]));可得到连续内存,且mat[i][j]合法;而若用int **mat则需多次分配,类型语义完全不同。下列代码展示数组指针管理连续二维内存的方式:
#include <stdlib.h>
#include <stdio.h>
int main() {
int (*mat)[5] = malloc(3 * sizeof(int[5]));
if (!mat) return 1;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 5; j++)
mat[i][j] = i * 10 + j;
printf("%dn", mat[2][3]); // 输出23
free(mat);
return 0;
}
最后建议在团队代码中尽量借助typedef或using(C++11)隐藏原始声明的繁琐。例如using Row = int[5]; Row* p;和int (*p)[5];等价,但前者让阅读者一眼看出p是行指针。当声明复杂到超过两层指针与数组混合时,拆别名几乎是必要的维护性手段,也能从根源上规避括号位置写错的问题。