第29章 高级指针应用:指针与二维数组、函数指针数组、复杂指针声明解析、回调函数实战
指针这东西,说简单也简单,说复杂能复杂到让你怀疑人生。我做了十几年C语言开发,见过太多人在指针上栽跟头。尤其是高级指针应用,很多人一看到二维数组指针、函数指针数组就头大。今天咱们就把这些硬骨头啃下来。
29.1 指针与二维数组:别被表象迷惑
二维数组在内存里其实是一维存储的。这个事实,我当年学的时候花了很长时间才真正接受。你想想看,int a[3][4] 在内存中就是12个int连续排列,没有所谓的“行”和“列”。
那指针怎么玩?看这个:
int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int (*p)[4] = a; // 指向包含4个int的数组的指针
int *q = &a[0][0]; // 指向第一个元素的指针
这里 p 和 q 有什么区别?
- p+1 跳过了4个int(一行)
- q+1 只跳过了1个int
说白了,p 是“行指针”,q 是“元素指针”。我在项目中处理图像数据时,经常用行指针来遍历像素行,效率比元素指针高不少。
核心理解: a[i][j] 等价于 *(*(a+i)+j)。先取第i行的地址,再偏移j个元素。
29.2 函数指针数组:把函数当数据用
函数指针数组,说白了就是一个数组,里面存的是函数地址。这个技巧在实现状态机、命令分发时特别好用。
我曾经写过一个网络协议解析器,不同消息类型对应不同处理函数。用函数指针数组,代码干净得像教科书:
#include <stdio.h>
void handle_login(void) { printf("处理登录\n"); }
void handle_logout(void) { printf("处理登出\n"); }
void handle_data(void) { printf("处理数据\n"); }
int main() {
// 函数指针数组
void (*handlers[3])(void) = {
handle_login,
handle_logout,
handle_data
};
int msg_type = 1; // 假设收到登出消息
if (msg_type >= 0 && msg_type < 3) {
handlers[msg_type](); // 直接调用
}
return 0;
}
嗯,这里要注意:函数指针数组的类型声明有点绕。void (*handlers[3])(void) 怎么读?
- handlers[3] 是一个数组
- (*handlers[3]) 数组元素是指针
- (*handlers[3])(void) 指针指向一个函数,参数是void
- void (*handlers[3])(void) 函数返回void
我建议你从右往左读,或者用typedef简化:
typedef void (*HandlerFunc)(void);
HandlerFunc handlers[3] = {handle_login, handle_logout, handle_data};
这样是不是清爽多了?
29.3 复杂指针声明解析:右左法则
复杂指针声明是C语言的“劝退”利器。什么“返回函数指针的函数的指针”之类的,看着就头疼。但我有一套方法——右左法则。
右左法则: 从标识符开始,先向右看,再向左看,遇到括号改变方向。
举个例子:
void (*signal(int, void (*)(int)))(int);
别慌,一步步来:
- 找到标识符 signal
- 向右看,遇到 (int, void (*)(int)),说明 signal 是一个函数,参数是int和一个函数指针
- 向左看,遇到 *,说明 signal 返回一个指针
- 再向右看,遇到 (int),说明这个指针指向一个函数,参数是int
- 再向左看,遇到 void,说明这个函数返回void
所以 signal 是:一个函数,参数是int和函数指针,返回一个指向函数的指针,该函数参数是int,返回void。
我的小技巧: 遇到复杂声明,先用typedef拆解。比如上面的signal,可以拆成:
typedef void (*Handler)(int);
Handler (*signal(int, Handler))(int);
虽然还是有点绕,但至少能看懂了。
29.4 回调函数实战:让代码活起来
回调函数,说白了就是把一个函数的地址传给另一个函数,让它在合适的时候调用你。这是实现“控制反转”的经典手法。
我在写一个排序工具时,需要支持多种排序规则。用回调函数,调用者想怎么排就怎么排:
#include <stdio.h>
// 比较函数类型
typedef int (*CompareFunc)(int, int);
int ascending(int a, int b) { return a - b; }
int descending(int a, int b) { return b - a; }
// 冒泡排序,接受比较函数作为回调
void bubble_sort(int arr[], int n, CompareFunc cmp) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (cmp(arr[j], arr[j+1]) > 0) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
int main() {
int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
int n = sizeof(arr)/sizeof(arr[0]);
bubble_sort(arr, n, ascending);
// 现在arr是升序
bubble_sort(arr, n, descending);
// 现在arr是降序
return 0;
}
避坑指南: 我曾经在回调函数中不小心修改了全局状态,导致多线程环境下数据错乱。回调函数一定要设计成“无副作用”的,或者明确文档说明其行为。
29.5 知识体系总览
下面这张图,是我整理的高级指针应用知识结构。你可以把它当作学习路线图:
29.6 实战经验总结
说了这么多,最后分享几个我个人的经验:
- 能用typedef就别手写复杂声明。 代码是给人看的,不是用来炫技的。
- 函数指针数组的长度最好用枚举常量控制。 我见过有人硬编码数字,后来加功能时忘了改数组大小,直接越界。
- 回调函数的参数尽量简单。 我曾经传了一个复杂的结构体指针,结果回调里各种解引用,代码又臭又长。
- 二维数组传参时,第二维长度必须明确。 这是C语言的硬性要求,别想着偷懒。
一句话总结: 高级指针应用的核心,就是理解“指针的类型决定了它如何解释内存”。把这个想通了,什么二维数组指针、函数指针数组,都是纸老虎。
公众号:蓝海资料掘金营,微信deep3321