在第十五篇初识指针时,我们说过“指针是存地址的变量”。在第十六篇,我们又遇到了“存指针地址的指针”——二级指针。那时候我们浅尝辄止,留了一个话头:为什么要用二级指针?三级指针、多级指针又是什么鬼?还有那些像咒语一样的复杂声明,到底该怎么读?
今天我们就来把这些“高阶指针”彻底拿下。读完你会觉得,什么 int *(*p[10])(int),不过是纸老虎。
一、从一级到二级:为什么要指向指针的指针?
一级指针 int *p 存的是一个 int 变量的地址。通过它,你可以修改那个 int 的值。
但如果你要修改的是指针本身呢? 比如,你想在一个函数里改变传入的指针的值(让它指向新分配的内存),那你就需要把那个指针的地址传进去——也就是 二级指针。
看这个场景:在函数内部分配内存,并将这个内存地址“传出去”给调用者。
#include <stdio.h>
#include <stdlib.h>
void allocate_array(int **ptr, int size) {
*ptr = (int*)malloc(size * sizeof(int)); // 修改 ptr 指向的那个指针
if (*ptr != NULL) {
for (int i = 0; i < size; i++) {
(*ptr)[i] = i * 10;
}
}
}
int main(void) {
int *arr = NULL;
allocate_array(&arr, 5); // 传 arr 的地址
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
free(arr);
return 0;
}
输出:
0 10 20 30 40
allocate_array 需要修改 main 里的 arr 变量本身(让它从 NULL 变成指向新内存的地址)。C 语言只有按值传递,所以我们必须把 arr 的地址 &arr 传进去。这个地址的类型就是 int**——指向 int* 的指针。
一级指针可以修改值,二级指针可以修改一级指针,以此类推。 这就是多级指针存在的根本逻辑。
二、二级指针与动态二维数组
第十六篇我们学过二维数组在内存中是行优先连续存储的。那种二维数组要求每一行长度相同。但有时我们需要一个“不规则的”二维数组,每一行的长度可以不同(比如存储变长字符串),或者我们需要在运行时决定行列大小,且不想被“矩形”约束。
这时就可以用二级指针 + 多次 malloc 来构造一个“指针数组”,每个指针指向一个独立的一维数组。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int rows = 3;
int **matrix = (int**)malloc(rows * sizeof(int*)); // 1. 分配指针数组
if (matrix == NULL) return 1;
// 2. 为每一行分配不同的长度
int cols[] = {2, 4, 3}; // 第一行2个元素,第二行4个,第三行3个
for (int i = 0; i < rows; i++) {
matrix[i] = (int*)malloc(cols[i] * sizeof(int));
if (matrix[i] == NULL) return 1;
}
// 3. 赋值
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols[i]; j++) {
matrix[i][j] = i * 10 + j;
}
}
// 4. 打印
for (int i = 0; i < rows; i++) {
printf("第 %d 行: ", i);
for (int j = 0; j < cols[i]; j++) {
printf("%2d ", matrix[i][j]);
}
printf("\n");
}
// 5. 释放(先释放每一行,再释放指针数组)
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
return 0;
}
输出:
第 0 行: 0 1
第 1 行: 10 11 12 13
第 2 行: 20 21 23
这种结构被称为“锯齿数组”或“Iliffe 向量”。它的优点是每行长度灵活;缺点是内存不连续,可能有缓存不友好的问题,而且释放时比较繁琐(要先释放每行,再释放行指针数组)。
图形化理解:
matrix (int**)
|
v
[0] ---> [0][1]
[1] ---> [10][11][12][13]
[2] ---> [20][21][22]
matrix 是一个 int**,它指向一个由 int* 组成的数组,每个 int* 又各自指向一个真正的 int 数组。
三、三级指针?多级指针的适度原则
理论上,可以声明三级、四级甚至更高级的指针,例如 int ***p;。它就是一个指向“指向 int* 的指针”的指针。每增加一级,就是多一层的间接访问。
int a = 5;
int *p1 = &a; // p1 -> a
int **p2 = &p1; // p2 -> p1 -> a
int ***p3 = &p2; // p3 -> p2 -> p1 -> a
printf("%d\n", ***p3); // 输出 5
但说实话,在常规编程中三级及以上指针极少使用。如果你发现自己需要三级指针,往往意味着需要重新审视数据结构设计,可能用结构体封装会更清晰。
多级指针的合理用途主要有:
- 二级指针:修改一级指针(函数内分配内存、链表头指针操作)、动态锯齿数组、命令行参数(
char **argv)。 - 三级指针:极偶尔用于修改二级指针(比如在函数内修改二维动态数组的结构),但多数情况下可以用结构体取代。
守则:能少一级就少一级,适度即可。
四、复杂声明的阅读:右左法则实战
学了这么多指针、数组、函数,它们混合在一起时,声明会变得像天书。比如:
int *(*p[10])(int);
这是什么鬼?我们上篇提到的“右左法则”就是解咒的钥匙。
右左法则(The Clockwise/Spiral Rule 的简化)
- 找到变量名,从它开始。
- 先向右看:遇到
[ ](数组)或( )(函数)就解析出来。 - 再向左看:遇到
*(指针)、const等就解析出来。 - 如果遇到括号
(),跳进去,重复 2-3 步。 - 一直处理到类型名,然后继续往外层扩展。
我们用几个例子练习。
例一:int *p[10]
- 变量名是
p。 - 向右看:
[10]→p是数组,有 10 个元素。 - 再向左看:
*→ 元素是指针。 - 再向左看:
int→ 指针指向int。 - 结论:
p是一个有 10 个元素的数组,每个元素是int*。(指针数组)
例二:int (*p)[10]
- 变量名
p,被括号包着,先处理括号内部。 - 括号内向右:没有东西。
- 括号内向左:
*→p是指针。 - 跳出括号,向右看:
[10]→ 指向有 10 个元素的数组。 - 向左看:
int→ 数组元素是int。 - 结论:
p是一个指针,指向有 10 个int的数组。(数组指针)
例三:int *(*p[10])(int)
- 变量名
p。 - 向右看:
[10]→p是数组,有 10 个元素。 - 向左看:
*→ 元素是指针。 - 再向右看:
(int)→ 这个指针指向函数,该函数接收一个int参数。 - 向左看:
*→ 函数返回指针。 - 再向左:
int→ 指针指向int。 - 结论:
p是一个有 10 个元素的数组,每个元素是一个函数指针,指向“接收一个int、返回int*”的函数。
看,其实并不神秘,只是需要耐心拆解。
例四:int (*func(int))(void)
这不是变量声明,而是函数声明。
func是函数名。- 向右看:
(int)→ 接收一个int参数。 - 向左看:
*→ 这个函数返回一个指针。 - 跳出,向右看:
(void)→ 返回的指针指向一个函数,该函数接收void。 - 向左看:
int→ 该函数返回int。 - 结论:
func是一个函数,接收int参数,返回一个函数指针,该指针指向“接收void返回int”的函数。
这种声明在实际项目中极其罕见,若遇到,通常都会用 typedef 简化。
五、typedef 是复杂声明的救星
当你遇到需要反复使用的复杂指针类型时,用 typedef 分步定义,是提升代码可读性的最佳实践。
比如上面那个“返回函数指针的函数声明”,正常人都会拆解:
typedef int (*func_ptr)(void); // func_ptr 是函数指针类型
func_ptr func(int); // func 是函数,接收 int,返回 func_ptr
清晰无比。所以,如果你发现自己在手写三级以上的指针或嵌套函数指针,立刻停下来,用 typedef 分步定义。这不仅是给自己看,更是给未来维护你代码的人(包括六个月后的你自己)行善。
六、常见错误与陷阱
1. 解引用级别搞错
int a = 10;
int *p = &a;
int **pp = &p;
printf("%d\n", *pp); // 错误!*pp 是 p(地址),不是 a
应该 **pp 才是 a。多级指针的解引用必须逐级剥开,心里默念:pp → *pp 是一级指针 → **pp 才是值。
2. 忘记为二级指针分配行指针数组
int **matrix;
matrix[0] = malloc(10 * sizeof(int)); // 错误!matrix 未初始化
必须先 matrix = malloc(rows * sizeof(int*)); 分配指针数组。
3. 释放顺序错误
int **matrix = malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++) matrix[i] = malloc(cols * sizeof(int));
free(matrix); // 错误!先释放了行指针数组,导致每行内存泄漏
必须先释放每一行的内存,再释放行指针数组。顺序反了就会泄漏内层数组。
4. 混淆指针数组和数组指针(再提醒)
int *arr[10]:arr是数组,元素是指针。int (*arr)[10]:arr是指针,指向数组。
每次看到这种声明,用右左法则读一遍,别靠感觉猜。
七、小结
多级指针和复杂声明不是魔法,而是一套有逻辑的体系:
- 一级指针
T*存的是T的地址。 - 二级指针
T**存的是T*的地址,常用于在函数内修改一级指针的值,或构造动态锯齿数组。 - 三级及以上指针极少出现在良好设计的代码中,如果出现,重新审视结构或用
typedef化简。 - 复杂声明用“右左法则”按部就班拆解,或者直接用
typedef分步构建可读的类型别名。
至此,我们已经完成了指针与内存管理阶段的所有核心内容。你已经有能力操作任何内存结构,理解任何指针声明。下一阶段,我们将进入结构化数据的世界——struct(结构体)、union(共用体)和 enum(枚举),让你能把不同类型的数据打包在一起,构建真正贴近现实的复杂数据模型。
课后小练习
- 写一个函数
void create_matrix(int ***mat, int rows, int cols),在函数内部分配一个rows × cols的二维动态数组(锯齿状),通过三级指针参数传回给调用者。并在main中验证并释放。 - 解释以下声明的含义,并尝试声明对应的变量并简单使用:
(提示:用右左法则,然后尝试用int *(*fp)(int); int (*arr[5])(double); char *(*(*p)(void))[10];typedef重写它们,看看是否更易读。) - 分析以下代码的错误:
它错在哪里?如何修正?int **p = malloc(3 * sizeof(int)); p[0][0] = 10; - (小挑战)实现一个能够处理命令行参数中多级子命令的简单框架:用二级指针
argv和函数指针数组,根据第一个参数的不同,调用不同的处理函数。处理函数打印自己的名字即可。体会多级指针和函数指针的结合。
我们下期见!

543

被折叠的 条评论
为什么被折叠?



