10、多级指针:二级指针的定义与使用、多级指针的应用场景、指针的指针与动态内存
说实话,很多C语言学习者学到指针就卡住了。等学到二级指针,更是直接想放弃。我当年也是这样,觉得指针套指针,这不是故意折腾人吗?
但后来做了几个项目,我才明白——二级指针不是炫技,是实实在在的刚需。今天我就把这块硬骨头给你啃下来。
10.1 二级指针的定义与基本使用
二级指针,说白了就是「指向指针的指针」。你想想看,普通指针存的是变量的地址,那二级指针存的就是指针变量的地址。
定义方式很简单:
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针
这里 pp 就是二级指针。它存的是 p 的地址,而 p 存的是 a 的地址。要拿到 a 的值,需要两次解引用:
printf("%d\n", **pp); // 输出 10
核心理解:
pp是二级指针变量,存的是p的地址*pp得到p的值(即&a)**pp得到a的值(即 10)
我个人习惯把二级指针想象成「两层抽屉」:第一层抽屉里放着一张纸条,纸条上写着第二层抽屉的位置。你要打开两次,才能拿到真正的东西。
10.2 二级指针的核心应用场景
二级指针到底有什么用?我总结了三个最常见的场景,都是我在项目中实打实用过的。
场景一:在函数中修改指针本身
这是最经典的应用。你想写一个函数,让它在外面把指针指向别处。如果传一级指针,你改不了——因为C语言是值传递。
看个例子:
#include <stdio.h>
#include <stdlib.h>
// 错误写法:传一级指针
void wrong_alloc(int *p) {
p = (int *)malloc(sizeof(int) * 10);
// 这里改的是形参,外面的指针没变!
}
// 正确写法:传二级指针
void right_alloc(int **pp) {
*pp = (int *)malloc(sizeof(int) * 10);
// *pp 就是外面的指针,这下改到了
}
int main() {
int *arr = NULL;
wrong_alloc(arr); // arr 还是 NULL
right_alloc(&arr); // arr 现在指向了堆内存
// 使用 arr...
free(arr);
return 0;
}
我曾经踩过的坑:刚入行时写了一个链表初始化函数,传的是一级指针。调试了一整天,发现链表头指针永远是 NULL。后来老同事看了一眼,说「你传二级指针试试」。嗯,从此我就记住了这个教训。
场景二:动态二维数组
用二级指针可以轻松创建动态二维数组。每一行的列数可以不同,这就是所谓的「不规则数组」。
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3;
int **matrix;
// 先分配行指针数组
matrix = (int **)malloc(rows * sizeof(int *));
// 再为每一行分配不同大小的列
matrix[0] = (int *)malloc(2 * sizeof(int)); // 第0行2列
matrix[1] = (int *)malloc(3 * sizeof(int)); // 第1行3列
matrix[2] = (int *)malloc(4 * sizeof(int)); // 第2行4列
// 使用...
// 释放:先释放每行,再释放行指针数组
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
return 0;
}
释放顺序很重要:先释放每行的数据,再释放行指针数组。顺序反了会导致内存泄漏。我见过有人先 free(matrix) 再 free(matrix[i]),那 matrix[i] 已经变成野指针了。
场景三:函数指针数组的指针
这个稍微进阶一点。当你需要管理一组函数指针时,二级指针就派上用场了。
#include <stdio.h>
void func1() { printf("功能1\n"); }
void func2() { printf("功能2\n"); }
void func3() { printf("功能3\n"); }
int main() {
// 函数指针数组
void (*func_arr[3])() = {func1, func2, func3};
// 二级指针指向函数指针数组
void (*(*pp)[3])() = &func_arr;
// 调用:(*pp)[i] 得到函数指针,再加()调用
(*pp)[0](); // 输出:功能1
(*pp)[1](); // 输出:功能2
return 0;
}
这个写法看着有点吓人,对吧?其实你只要记住:void (*(*pp)[3])() 的意思是「pp 是一个指针,指向一个包含3个函数指针的数组」。拆开看就没那么可怕了。
10.3 多级指针的扩展:三级指针及以上
理论上,指针可以无限嵌套。三级指针就是指向二级指针的指针,四级指针就是指向三级指针的指针……
int a = 10;
int *p = &a;
int **pp = &p;
int ***ppp = &pp;
int ****pppp = &ppp;
printf("%d\n", ****pppp); // 输出 10
实际开发中的建议:三级指针已经很少见了,四级以上基本不会出现。如果你发现代码里用了四级指针,多半是设计有问题。我做了十几年嵌入式,三级指针只用过两次,四级指针一次都没用过。
10.4 指针的指针与动态内存管理
二级指针在动态内存管理中扮演着重要角色。除了前面说的分配二维数组,还有一个常见用法:在函数内部修改指针数组。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 在函数内部为字符串指针数组分配内存
void create_strings(char ***pp_arr, int count) {
// 分配指针数组
*pp_arr = (char **)malloc(count * sizeof(char *));
// 为每个指针分配字符串
for (int i = 0; i < count; i++) {
(*pp_arr)[i] = (char *)malloc(20 * sizeof(char));
sprintf((*pp_arr)[i], "字符串%d", i + 1);
}
}
int main() {
char **str_arr = NULL;
create_strings(&str_arr, 3);
for (int i = 0; i < 3; i++) {
printf("%s\n", str_arr[i]);
free(str_arr[i]);
}
free(str_arr);
return 0;
}
你看,这里用了三级指针 char ***pp_arr。为什么?因为我们要修改的是 char ** 类型的变量(即 str_arr),所以必须传它的地址,也就是 char ***。
记忆口诀:要修改什么类型的变量,就传什么类型的指针。要修改 int,传 int*;要修改 int*,传 int**;要修改 int**,传 int***。以此类推。
10.5 知识体系总览
下面这张图把二级指针的核心知识点串起来了,建议你多看几遍:
10.6 避坑指南与最佳实践
二级指针用不好,很容易出问题。我把自己踩过的坑和总结的经验分享给你:
| 常见问题 | 原因 | 解决方案 |
|---|---|---|
| 传一级指针想修改指针本身 | 值传递,形参修改不影响实参 | 传二级指针(指针的地址) |
| 二级指针未初始化就解引用 | 野指针,指向未知内存 | 初始化为 NULL,使用前检查 |
| 释放顺序错误导致内存泄漏 | 先释放外层指针,内层变野指针 | 先释放内层,再释放外层 |
| 函数返回局部指针的地址 | 局部变量在函数返回后销毁 | 用 malloc 分配,或传二级指针 |
我曾经犯过的错:有一次写一个树形结构的销毁函数,传了一级指针进去,在函数内部 free 了节点,但忘了把外面的指针置 NULL。结果后面代码判断指针是否为 NULL 时,以为节点还在,继续访问——直接段错误。从那以后,凡是需要修改指针本身的函数,我统一用二级指针。
10.7 小结
二级指针其实没那么神秘。你只要记住一句话:当你想在函数里改变一个指针的值时,就传它的地址——也就是二级指针。
至于三级、四级指针,知道有这回事就行。实际项目中,二级指针已经能解决绝大多数问题。把二级指针吃透,你的C语言功力就能上一个台阶。
嗯,今天就聊到这里。多写代码,多调试,慢慢就熟练了。