指针进阶:二级指针、函数指针、void指针与const指针
指针这东西,很多初学者觉得难,其实说白了就是「地址的搬运工」。今天咱们聊点进阶的——二级指针、函数指针、void指针,还有const跟指针的爱恨情仇。这些内容我在实际项目中几乎天天打交道,踩过的坑也不少,今天一并分享给你。
一、二级指针:指针的指针
一级指针存的是变量的地址。二级指针呢?它存的是指针的地址。你想想看,如果我想在函数里修改一个指针本身的值(不是它指向的内容),就得用二级指针。
核心理解:二级指针就是「指向指针的指针」。用两个星号声明:int **pp。
1.1 二级指针的声明与使用
int a = 10;
int *p = &a; // 一级指针
int **pp = &p; // 二级指针
printf("a = %d\n", **pp); // 输出 10
我在项目中遇到过这样一个场景:写一个链表库,需要在函数里修改头指针。如果只传一级指针,函数里改的是副本,外面的头指针纹丝不动。这时候二级指针就派上用场了。
1.2 二级指针的典型应用:动态二维数组
int **matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)malloc(cols * sizeof(int));
}
嗯,这里要注意:释放的时候顺序要反过来,先释放每一行,再释放行指针数组。我曾经因为搞反了顺序,内存泄漏查了一下午。
避坑指南:二级指针做函数参数时,记得用 int **ptr 而不是 int *ptr。前者才能修改指针本身的值。
二、函数指针:把函数当变量用
函数也有地址。你想想看,代码编译后,函数就躺在内存的代码段里,入口地址就是函数名。我们可以用指针存这个地址,然后通过指针调用函数。
2.1 函数指针的声明
// 声明一个函数指针,指向返回int、参数为两个int的函数
int (*funcPtr)(int, int);
// 赋值
funcPtr = &add; // 或者直接 funcPtr = add;
// 调用
int result = funcPtr(3, 5); // 或者 (*funcPtr)(3, 5)
我个人习惯用 funcPtr(3, 5) 这种写法,简洁明了。两种写法等价,选你喜欢的就好。
2.2 回调函数:让代码更灵活
回调函数说白了就是「你把函数地址传给我,我合适的时候调用它」。标准库里的 qsort 就是典型例子。
int compare(const void *a, const void *b) {
return *(int *)a - *(int *)b;
}
int arr[] = {5, 3, 8, 1, 9};
qsort(arr, 5, sizeof(int), compare);
我在做嵌入式驱动开发时,经常用回调函数处理中断。注册一个回调,中断来了自动调用,代码结构清晰多了。
小技巧:用 typedef 给函数指针起个别名,代码可读性会好很多:typedef int (*CompareFunc)(const void *, const void *);
三、void指针:万能容器
void * 可以指向任何类型的数据。但它有个限制:不能直接解引用。你得先把它转成具体类型。
int a = 100;
void *vp = &a;
// printf("%d\n", *vp); // 错误!不能解引用void指针
printf("%d\n", *(int *)vp); // 正确,先转类型再解引用
void指针最常见的用途就是写通用函数。memcpy、memset、qsort 这些库函数都用 void * 来接受任意类型的数据。
注意:void指针做指针运算时,步长是1字节。不像 int * 步长是4字节。这个细节容易踩坑。
四、const与指针:四种组合
const和指针组合起来有四种情况,我当年学的时候也绕了一阵。其实记住一句话就行:const修饰谁,谁就不能变。
| 写法 | 含义 | 示例 |
|---|---|---|
const int *p |
指针指向的值不能改 | *p = 10; // 错误 |
int *const p |
指针本身不能改 | p = &b; // 错误 |
const int *const p |
指针和值都不能改 | 两个操作都错误 |
int const *p |
等价于 const int *p |
值不能改 |
我个人习惯把 const 写在类型前面,比如 const int *p,读起来更自然:「这是一个const int的指针」。
4.1 const指针的实际应用
在嵌入式开发中,const指针常用于定义只读的外设寄存器地址:
// GPIO输出数据寄存器,地址固定,内容可写
volatile uint32_t *const GPIO_OUT = (uint32_t *)0x40020C14;
// 系统唯一ID,只读
const uint32_t *DEVICE_ID = (uint32_t *)0x1FFF7A10;
我曾经在项目里犯过一个低级错误:把const指针当普通指针用,试图修改它指向的值。编译器报错时我还愣了半天——嗯,后来养成了习惯,定义指针时先想清楚「哪个不能变」。
五、知识体系总览
下面这张图帮你理清本章的知识结构:
六、总结与建议
指针进阶这块,说白了就是多练。我建议你从三个方向入手:
- 二级指针:写一个链表或树的操作函数,体会传指针和传二级指针的区别
- 函数指针:实现一个简单的计算器,用函数指针数组来调度加减乘除
- const指针:在现有代码里加上合适的const修饰,看看编译器能帮你发现哪些潜在问题
这些内容看起来多,其实核心就一句话:指针是地址,类型决定怎么解释这个地址。搞懂了这个,二级指针、函数指针、void指针都是同一个道理的不同表现形式。
好了,今天先聊到这儿。代码写多了,这些概念自然就熟了。
公众号:蓝海资料掘金营,微信deep3321