指针这东西,聊到第三篇,说明你已经跨过了“指针是什么”“指针怎么读写变量”“指针和数组到底啥关系”这些坎,开始往进阶区走了。这篇我打算把指针体系里最绕的几个点一次性理清楚:数组指针和指针数组怎么区分、二级指针为什么在函数传参里绕不开、函数指针和回调到底怎么落地,以及笔试面试里那些“看上去简单一做就错”的指针题到底在考什么。这篇文章适合正在学C语言准备考试的朋友,也适合写嵌入式、写底层代码时需要跟内存打交道的开发者,看完能帮你把指针这块拼图补完整。
同时我也想说一句:指针是C语言里最容易被误解、也最值得花时间吃透的部分。前面两篇讲的是“怎么用”,这一篇更多是“怎么用对”“怎么用活”。我会把实际开发中踩过的坑、调过的错也一并写进来,尽量让每个概念都能落地,而不是停留在教科书层面的定义。
1. 数组指针与指针数组:定义只差一个括号,行为差之千里
1.1 先分清谁是谁:优先级决定身份
先说一个我见过无数人翻车的现象:int *p[5] 和 int (*p)[5] 这俩声明,看着差不多,实际上一个是“数组”,一个是“指针”。判断方法很简单,看 p 先跟谁结合:* 的优先级低于 [],所以 int *p[5] 里 p 先跟 [5] 结合,说明它是一个数组,数组里面每个元素都是 int* 类型;而 int (*p)[5] 里 p 先跟 * 结合,说明它是一个指针,指针指向的是一整个长度为 5 的 int 数组。
用人话讲:指针数组是“装着指针的盒子”,数组指针是“指向盒子的绳”。这个类比虽然土,但在最初阶段帮助很大。我曾经带过的一个实习生,在写命令解析器的时候定义了一个 char *args[10],想往里面塞字符串,结果一不留神写成了 char (*args)[10],编译直接报错。后来我让他用“先结合”这个方法去分析,一句话就点通了。
实际写代码的时候,我建议大家不要靠“背”来区分,靠分析。看到声明就先问自己:p 旁边那个 * 有没有被括号跟 p 绑在一起?绑了就说明指针身份优先;没绑、被 [] 抢了先,那它就是数组。这个习惯练出来了,后面看任何复杂声明都不慌。
1.2 二维数组传参:数组指针的实战场景
数组指针最常见的应用场景是二维数组的函数传参。很多人写二维数组传参的时候习惯这样写:
c复制void printMatrix(int arr[3][4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
这么写没问题,但换个角度看其实有隐患:int arr[3][4] 这种写法在形参里会被“退化”成 int (*arr)[4],也就是数组指针。既然本质是数组指针,为什么不直接把它写出来呢?直接写出来有个好处,就是别人看函数声明时能立刻明白参数结构,而不是靠“读代码猜形状”。
正确写法是这样的:
c复制void printMatrix(int (*arr)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
这两种写法语义完全等价,但后者更清楚地表明:这个函数接收的是一个“指向一行(4个整数)的指针”。理解了这一点,你就能明白为什么二维数组传参时第二维必须写死——因为指针在做 arr + 1 运算时,需要知道每一行到底有多长,才能准确跳转到下一行。如果你不指定列数,编译器就不知道一次移动多少个字节。
这里有个细节容易踩坑:如果你试图写 void printMatrix(int **arr) 去接收一个二维数组,编译时会报警告,运行后大概率错误。因为二维数组名是“指向数组的指针”,它的层级跟“指向指针的指针”并不一样。虽然有些场景下 int** 可以用来访问指针数组,但拿来访问真正意义上的二维数组,步长是不匹配的。这个坑我见过不少刚接触指针的人往里跳。
1.3 指针数组的经典用法:保存字符串集合
指针数组最典型的实战场景是保存一堆字符串。比如写一个小命令行工具,需要把用户输入的多个参数存起来。这时你可以这样做:
c复制char *commands[] = {
"help",
"version",
"quit",
"status"
};
commands 是一个数组,里面有 4 个 char* 元素,每个元素保存一个字符串常量的首地址。这样做的好处是:字符串本身不拷贝,数组里只存地址,省内存、初始化也快。访问的时候 commands[0] 拿到的就是 "help" 这个字符串的首地址,直接 printf("%s\n", commands[0]) 就能打印。
如果用二维字符数组 char commands[4][10],那每一行都要预留 10 个字节,如果字符串长短不一,空间浪费就很明显。尤其当你处理的字符串很多、长度差异很大的时候,指针数组的优势非常突出。我在实际项目里写菜单表、协议解析表、命令映射表时,几乎都是指针数组的标准用法。
c复制// 命令映射表,配合函数指针可以做成一个迷你调度器
struct Command {
char *name;
void (*handler)(void);
};
等等,函数指针这块我后面专门讲,这里先留个钩子。反正你记住:指针数组是把“地址”当成集合元素来管理,这在处理一组字符串、一组函数、一组对象句柄的时候都特别好用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 二级指针:函数里修改外部指针的唯一方案
2.1 为什么需要二级指针:值传递的困局
在C语言里,函数参数默认是按值传递的。这意味着形参是实参的拷贝,你在函数里改形参,外面不会变。这个大家都知道,但放到指针身
