15、二级指针:从入门到实战

说实话,二级指针是很多C语言学习者的"拦路虎"。我记得刚入行那会儿,看到代码里出现int **p这种写法,第一反应就是——这玩意儿到底存的啥?

其实没那么玄乎。二级指针,说白了就是"指向指针的指针"。你想想看,普通指针存的是变量的地址,那二级指针存的就是指针变量的地址。就这么简单。

核心理解: 一级指针 → 指向变量;二级指针 → 指向一级指针。

15.1 二级指针的概念

先看个最直观的例子:

int a = 10;
int *p = &a;   // 一级指针,存a的地址
int **pp = &p; // 二级指针,存p的地址

printf("a = %d\n", a);       // 直接访问
printf("*p = %d\n", *p);     // 一级指针解引用
printf("**pp = %d\n", **pp); // 二级指针解引用两次

画个图你就明白了:

变量 a 值: 10 指针 p 存 &a 二级指针 pp 存 &p *p → 得到 a *pp → 得到 p **pp → 得到 a a = 10 p = &a pp = &p
我的小技巧: 看到二级指针,你就想"要拿到最终的值,得解引用两次"。第一次拿到指针,第二次拿到数据。

15.2 二级指针与指针数组

指针数组,就是数组里每个元素都是指针。这东西在字符串处理时特别常用。

// 指针数组:每个元素都是 char*
char *names[] = {"Alice", "Bob", "Charlie", "David"};

// 这里的 names 是什么类型?
// 它退化后是 char ** 类型!
// 因为 names[0] 是 char*,所以 names 是 char**

我在项目中处理过配置文件解析,经常用指针数组来管理多行字符串。你想想看,如果用二维字符数组,每行长度固定,浪费空间不说,还不好扩展。指针数组就灵活多了。

void print_names(char **arr, int count) {
    for (int i = 0; i < count; i++) {
        printf("Name %d: %s\n", i, arr[i]);
        // arr[i] 等价于 *(arr + i)
    }
}

int main() {
    char *names[] = {"Alice", "Bob", "Charlie"};
    print_names(names, 3);  // 数组名退化为 char**
    return 0;
}
注意: 指针数组和二维数组在内存布局上完全不同。指针数组的每个元素指向独立的内存区域,而二维数组是连续存储的。千万别搞混了!

15.3 二级指针作为函数参数

这是二级指针最实用的场景之一。什么时候需要传二级指针?当函数内部要修改指针本身的值时。

举个例子:

// 错误示范:传一级指针,无法修改指针本身
void allocate_wrong(int *p, int size) {
    p = (int*)malloc(size * sizeof(int));
    // 这里修改的是形参p,对实参没影响
}

// 正确做法:传二级指针
void allocate_correct(int **p, int size) {
    *p = (int*)malloc(size * sizeof(int));
    // *p 就是实参指针本身,修改生效
}

int main() {
    int *arr = NULL;
    allocate_correct(&arr, 10);  // 传指针的地址
    // 现在 arr 指向了堆上分配的内存
    free(arr);
    return 0;
}

我曾经在写一个链表库时踩过这个坑。当时想在函数里初始化链表头指针,传了一级指针进去,结果函数返回后头指针还是NULL。查了半天才发现——嗯,得用二级指针。

黄金法则: 如果函数要修改指针指向的内容,传一级指针;如果函数要修改指针本身(比如分配内存),传二级指针。

15.4 动态分配二维数组

动态分配二维数组,说白了就是"先分配行指针数组,再给每行分配空间"。这是二级指针的经典应用。

int **create_matrix(int rows, int cols) {
    // 1. 分配行指针数组
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;

    // 2. 为每一行分配空间
    for (int i = 0; i < rows; i++) {
        matrix[i] = (int*)malloc(cols * sizeof(int));
        if (matrix[i] == NULL) {
            // 分配失败,释放已分配的行
            for (int j = 0; j < i; j++) {
                free(matrix[j]);
            }
            free(matrix);
            return NULL;
        }
    }
    return matrix;
}

void free_matrix(int **matrix, int rows) {
    if (matrix == NULL) return;
    for (int i = 0; i < rows; i++) {
        free(matrix[i]);
    }
    free(matrix);
}

// 使用示例
int main() {
    int rows = 3, cols = 4;
    int **mat = create_matrix(rows, cols);

    // 像普通二维数组一样使用
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            mat[i][j] = i * cols + j;
        }
    }

    free_matrix(mat, rows);
    return 0;
}

这里有个细节我想强调一下:matrix[i][j] 这个写法,编译器会把它解析为 *(*(matrix + i) + j)。先通过二级指针找到第i行的指针,再通过一级指针找到第j个元素。

避坑指南: 我曾经在释放二维数组时只free了matrix,忘了先free每行。结果内存泄漏,程序跑了几天后崩了。记住:先释放每行,再释放行指针数组。

还有一种更高效的方式——一次性分配所有内存:

// 连续内存分配法
int **create_matrix_contiguous(int rows, int cols) {
    int **matrix = (int**)malloc(rows * sizeof(int*));
    if (matrix == NULL) return NULL;

    // 一次性分配所有数据空间
    matrix[0] = (int*)malloc(rows * cols * sizeof(int));
    if (matrix[0] == NULL) {
        free(matrix);
        return NULL;
    }

    // 设置每行的起始地址
    for (int i = 1; i < rows; i++) {
        matrix[i] = matrix[0] + i * cols;
    }

    return matrix;
}

// 释放时也简单
void free_matrix_contiguous(int **matrix) {
    if (matrix == NULL) return;
    free(matrix[0]);  // 释放数据块
    free(matrix);     // 释放行指针数组
}

这种方式的好处是内存连续,CPU缓存命中率高。我在做图像处理项目时就用这种方案,性能提升很明显。

总结一下: 二级指针就是"指向指针的指针"。它最常用的三个场景:处理指针数组、作为函数参数修改指针本身、动态分配二维数组。掌握了这些,你就能应付绝大多数情况了。

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