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 知识体系总览

下面这张图把二级指针的核心知识点串起来了,建议你多看几遍:

二级指针知识体系 二级指针 int **pp 定义与基本使用 核心应用场景 动态内存管理 int **pp = &p; 两次解引用 **pp 修改指针本身 动态二维数组 函数指针数组 malloc/free 不规则数组 释放顺序

10.6 避坑指南与最佳实践

二级指针用不好,很容易出问题。我把自己踩过的坑和总结的经验分享给你:

常见问题 原因 解决方案
传一级指针想修改指针本身 值传递,形参修改不影响实参 传二级指针(指针的地址)
二级指针未初始化就解引用 野指针,指向未知内存 初始化为 NULL,使用前检查
释放顺序错误导致内存泄漏 先释放外层指针,内层变野指针 先释放内层,再释放外层
函数返回局部指针的地址 局部变量在函数返回后销毁 用 malloc 分配,或传二级指针

我曾经犯过的错:有一次写一个树形结构的销毁函数,传了一级指针进去,在函数内部 free 了节点,但忘了把外面的指针置 NULL。结果后面代码判断指针是否为 NULL 时,以为节点还在,继续访问——直接段错误。从那以后,凡是需要修改指针本身的函数,我统一用二级指针。

10.7 小结

二级指针其实没那么神秘。你只要记住一句话:当你想在函数里改变一个指针的值时,就传它的地址——也就是二级指针。

至于三级、四级指针,知道有这回事就行。实际项目中,二级指针已经能解决绝大多数问题。把二级指针吃透,你的C语言功力就能上一个台阶。

嗯,今天就聊到这里。多写代码,多调试,慢慢就熟练了。


公众号:蓝海资料掘金营,微信deep3321