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); // 二级指针解引用两次
画个图你就明白了:
我的小技巧: 看到二级指针,你就想"要拿到最终的值,得解引用两次"。第一次拿到指针,第二次拿到数据。
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