第十三章:二级指针——指针的指针,到底在搞什么名堂?
说实话,很多C语言学习者一碰到二级指针就开始犯怵。我当年刚入行时也一样,觉得这东西太绕了——指针已经够烦了,你还来个指向指针的指针?但后来在嵌入式项目里被坑过几次,才真正明白二级指针到底有多重要。
今天咱们就把二级指针掰开揉碎了讲清楚。你想想看,它本质上就是个“套娃”结构,但用好了,能解决很多一级指针搞不定的问题。
13.1 二级指针的定义:不就是个套娃吗?
二级指针,说白了就是“指向指针的指针”。我们平时用的 int *p 是一级指针,它存的是某个变量的地址。那二级指针 int **pp 呢?它存的是另一个指针变量的地址。
看个最直白的例子:
int a = 10;
int *p = &a; // p 存的是 a 的地址
int **pp = &p; // pp 存的是 p 的地址
这个关系图其实很简单:
pp --> p --> a
[0x100] [0x200] [10]
pp 里存的是 0x100(p 的地址),p 里存的是 0x200(a 的地址),a 里存的是 10。要拿到 a 的值,你得解引用两次:**pp。
核心理解: 一级指针操作变量,二级指针操作指针本身。你如果想让函数内部修改外部指针的指向,就必须用二级指针。
我在项目中遇到过这样一个场景:一个 RTOS 的任务控制块链表,头指针需要被多个函数修改。如果只传一级指针进去,函数内部改了指向,外面根本不知道——这就是典型的“值传递”陷阱。
13.2 二级指针与指针数组:这俩是天生一对
指针数组,就是数组里每个元素都是指针。比如:
char *str_arr[3] = {"Hello", "World", "C语言"};
这个 str_arr 本身是什么类型?它是一个数组,数组名代表首元素地址。首元素是 char * 类型,所以 str_arr 的类型就是 char **。
嗯,这里要注意:指针数组的名字,本质上就是一个二级指针常量。你不能改它的值,但可以把它赋值给一个二级指针变量:
char **pp_str = str_arr;
printf("%s\n", pp_str[0]); // 输出 "Hello"
为什么能这么用?因为 pp_str[i] 等价于 *(pp_str + i),而 str_arr[i] 也是同样的操作。说白了,二级指针和指针数组在内存布局上是完全兼容的。
我曾经在做一个命令行解析器时,就用指针数组存了一堆命令字符串,然后用二级指针遍历它们。代码大概长这样:
char *cmds[] = {"help", "reset", "status", "config"};
char **p = cmds;
for (int i = 0; i < 4; i++) {
printf("Command %d: %s\n", i, p[i]);
}
简单、直接、高效。嵌入式环境里,这种写法比用二维数组省内存得多——每个字符串可以不同长度,不会浪费空间。
小技巧: 如果你需要动态管理一组字符串(比如从配置文件里读出来的键值对),用 char ** 配合 malloc 逐行分配,比用二维数组灵活得多。
13.3 二级指针作为函数参数:这才是重头戏
二级指针最经典的应用场景,就是在函数内部修改外部指针的指向。我敢说,80% 的二级指针使用场景都跟这个有关。
先看一个反面教材——用一级指针试图修改外部指针:
void alloc_bad(int *p) {
p = (int *)malloc(sizeof(int) * 10); // 只修改了形参
}
int main() {
int *arr = NULL;
alloc_bad(arr);
// arr 仍然是 NULL!内存泄漏了!
}
为什么会这样?因为 C 语言是值传递。函数里的 p 是 arr 的副本,你改了副本,原件纹丝不动。这就是我曾经踩过的坑——调试了一下午才发现指针根本没被修改。
正确的做法是用二级指针:
void alloc_good(int **p) {
*p = (int *)malloc(sizeof(int) * 10);
if (*p == NULL) {
// 处理分配失败
}
}
int main() {
int *arr = NULL;
alloc_good(&arr);
// arr 现在指向了堆内存
// 用完记得 free(arr)
}
这里的关键是:&arr 传进去的是 arr 这个指针变量的地址。函数内部通过 *p 修改了那个地址里存的值——也就是 arr 本身。
再举一个嵌入式里常见的例子——链表操作。比如你要在链表头部插入节点,头指针需要更新:
typedef struct Node {
int data;
struct Node *next;
} Node;
void insert_head(Node **head, int value) {
Node *new_node = (Node *)malloc(sizeof(Node));
new_node->data = value;
new_node->next = *head;
*head = new_node; // 修改外部头指针
}
如果你只传一级指针 Node *head,那函数内部改了 head 的指向,外面的头指针还是指向原来的节点——链表就断掉了。
避坑指南: 我曾经在写一个内存池管理模块时,忘了给二级指针参数判空。结果传入 NULL 指针,函数里直接 *p = ... 就崩溃了。记住:使用二级指针前,一定要检查传入的指针本身是否为 NULL。
13.4 一张图看懂二级指针的核心逻辑
下面这张 SVG 图,把二级指针的定义、与指针数组的关系、作为函数参数的使用场景都串起来了。建议你仔细看看,理解清楚每个箭头代表什么。
13.5 几个必须记住的要点
最后,我把二级指针的几个关键点再拎出来强调一下。这些都是我在实际项目中反复验证过的:
- 声明语法:
int **pp;读作“指向 int 指针的指针”。从右往左读:pp是*指针,指向*指针,指向int。 - 解引用次数: 一级指针解一次,二级指针解两次。
*pp得到一级指针,**pp得到最终数据。 - 常见用途: 动态二维数组、字符串数组管理、链表/树等数据结构的修改操作、函数内部分配内存并返回给调用者。
- 容易犯的错: 把
int **和int *[]搞混、忘记给二级指针判空、在函数里只修改了形参没修改实参。
我的个人习惯: 在嵌入式项目里,我一般会在函数参数中用 int **pp 而不是 int *p[]。虽然两者在某些上下文中等价,但 ** 的写法更明确地告诉读者:“这个参数是用来修改指针本身的”。代码的可读性,往往就体现在这些细节里。
二级指针这东西,你刚开始觉得绕,但用多了就会发现它其实很自然。说白了,它就是 C 语言给你的一把钥匙,让你能直接操作指针变量本身。下次遇到需要修改外部指针的场景,别犹豫,上二级指针就对了。