第十二章:指针(三):函数指针、回调函数、指针作为函数参数、指针函数
指针这东西,说到函数这块,才真正开始有意思起来。
前面我们聊了指针和数组、指针和字符串,那都是基本功。今天要讲的这几个概念——函数指针、回调函数、指针作为函数参数、指针函数——才是真正让C语言变得灵活、高效、甚至有点“高级”的关键。
我个人习惯把这一章叫做“指针的进阶玩法”。你想想看,函数本身在内存里也是有地址的,那我们能不能像操作变量一样操作函数?当然可以。这就是函数指针的由来。
12.1 函数指针:把函数当变量用
函数指针,说白了就是一个指针变量,它存的是函数的入口地址。
定义方式有点绕,但记住一个口诀就行:“先写返回类型,括号里写指针名,再写参数列表”。
// 定义一个函数指针
int (*funcPtr)(int, int);
// 指向一个具体的函数
int add(int a, int b) {
return a + b;
}
funcPtr = add; // 或者 &add,效果一样
// 通过函数指针调用函数
int result = funcPtr(3, 5); // result = 8
嗯,这里要注意:int *funcPtr(int, int) 和 int (*funcPtr)(int, int) 是完全不同的东西。前者是一个返回指针的函数,后者才是指向函数的指针。少一个括号,意思全变了。
刚入行时,我写了一个复杂的回调系统,函数指针定义里漏了括号,结果编译器报了一堆莫名其妙的类型不匹配错误。查了整整一下午才发现是括号问题。从那以后,我每次写函数指针都会先写括号,再写星号和名字。
12.2 函数指针数组:批量管理函数
单个函数指针还不够过瘾?那就来一打。
函数指针数组,就是数组里每个元素都是一个函数指针。这在实现状态机、命令分发、菜单系统时特别有用。
// 定义三个函数
void open(void) { printf("打开\n"); }
void close(void) { printf("关闭\n"); }
void reset(void) { printf("重置\n"); }
// 函数指针数组
void (*cmdTable[])(void) = {open, close, reset};
// 通过索引调用
int cmd = 1;
cmdTable[cmd](); // 输出:关闭
我在项目中遇到过类似的需求:一个嵌入式设备有几十个命令,每个命令对应一个处理函数。如果用 switch-case 写,代码又长又丑。用函数指针数组,一行代码就搞定了分发逻辑。代码量直接砍半。
12.3 回调函数:把函数当参数传
回调函数,本质上就是函数指针作为参数传递给另一个函数。被调用的函数在合适的时机“回头”调用你传进来的函数。
为什么要这么干?因为有些行为是延迟决定的。比如你写一个排序函数,排序算法是固定的,但比较规则可以交给调用者决定。
// 回调函数类型
typedef int (*CompareFunc)(int, int);
// 排序函数,接受回调
void bubbleSort(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 asc(int a, int b) { return a - b; }
// 降序比较
int desc(int a, int b) { return b - a; }
// 使用
int data[] = {3, 1, 4, 1, 5, 9};
bubbleSort(data, 6, asc); // 升序
bubbleSort(data, 6, desc); // 降序
你看,排序算法本身不用改,只需要换一个回调函数,行为就完全变了。这就是回调的威力。
回调函数让代码从“写死”变成“可配置”。你写一个框架,别人用你的框架时只需要提供自己的回调函数,就能定制行为。这就是很多库和操作系统的设计思路。
12.4 指针作为函数参数:传地址,不传值
这个其实前面已经用过很多次了。但这里我要强调一个点:传指针不是为了炫技,而是为了修改实参。
C语言是值传递。你传一个变量进函数,函数里改的是副本,外面不受影响。但如果你传的是指针,函数就能通过指针修改原始数据。
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int x = 10, y = 20;
swap(&x, &y); // 现在 x=20, y=10
还有一个常见用法:用指针参数返回多个值。一个函数只能有一个返回值,但如果你需要返回两个以上的结果,就可以用指针参数“带出来”。
void splitTime(int totalSeconds, int *hours, int *minutes, int *seconds) {
*hours = totalSeconds / 3600;
*minutes = (totalSeconds % 3600) / 60;
*seconds = totalSeconds % 60;
}
我写函数时,如果参数是指针且只用于输入,我会加 const 修饰。这样既明确意图,又能防止误修改。比如 void print(const int *p),一看就知道这个指针是只读的。
12.5 指针函数:返回指针的函数
指针函数,就是返回值是指针的函数。注意和函数指针区分:指针函数是“返回指针的函数”,函数指针是“指向函数的指针”。
// 指针函数:返回 int 指针
int* findMax(int arr[], int n) {
int *max = &arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] > *max) {
max = &arr[i];
}
}
return max;
}
指针函数有个大坑:不能返回局部变量的地址。局部变量在函数返回后就销毁了,返回它的地址,外面拿到的是野指针。
// ❌ 错误示例
int* badFunc(void) {
int local = 100;
return &local; // local 在函数结束后就没了
}
// ✅ 正确做法:返回静态变量、全局变量、或者动态分配的内存
int* goodFunc(void) {
static int value = 100;
return &value;
}
有一次我写一个字符串处理函数,返回了局部字符数组的指针。调试时数据是对的,但放到正式环境里,数据全乱了。查了半天才发现是返回了栈上的地址。从那以后,我只要写指针函数,第一件事就是确认返回的指针指向的内存生命周期是否足够长。
12.6 综合应用:一个简单的命令分发器
把上面这些知识点串起来,写一个实用的例子。
#include <stdio.h>
#include <string.h>
// 命令处理函数类型
typedef void (*CmdHandler)(const char *args);
// 命令结构体
typedef struct {
const char *name;
CmdHandler handler;
const char *help;
} Command;
// 具体命令实现
void cmd_help(const char *args) {
printf("显示帮助信息\n");
}
void cmd_reboot(const char *args) {
printf("系统重启中...\n");
}
void cmd_version(const char *args) {
printf("版本 1.0.0\n");
}
// 命令表
Command cmdTable[] = {
{"help", cmd_help, "显示帮助"},
{"reboot", cmd_reboot, "重启系统"},
{"version", cmd_version, "显示版本"},
};
int cmdCount = sizeof(cmdTable) / sizeof(cmdTable[0]);
// 分发函数
void dispatch(const char *cmdName, const char *args) {
for (int i = 0; i < cmdCount; i++) {
if (strcmp(cmdName, cmdTable[i].name) == 0) {
cmdTable[i].handler(args);
return;
}
}
printf("未知命令: %s\n", cmdName);
}
int main() {
dispatch("version", NULL);
dispatch("reboot", NULL);
dispatch("help", NULL);
return 0;
}
这个例子用到了函数指针数组、回调函数、结构体指针。实际项目中,命令表可以放在 Flash 里,甚至支持动态注册。这就是指针在嵌入式系统中的典型应用。
12.7 本章知识体系
下面这张图帮你理清本章的核心脉络:
12.8 避坑指南
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路。
- 函数指针类型不匹配:声明和赋值时,返回类型和参数列表必须完全一致。不一致时编译器可能只给警告,运行时直接崩溃。
- 回调函数中的空指针检查:调用回调函数之前,一定要判断函数指针是否为 NULL。我曾经因为没做这个检查,在系统启动时直接跑飞了。
- 指针函数返回局部变量:这个前面说过了,返回栈上地址是未定义行为。用静态变量、全局变量或者动态内存分配来解决。
- 函数指针数组越界:用索引访问函数指针数组时,一定要做边界检查。否则调到一个非法地址,后果很严重。
- const 修饰的指针参数:如果函数不需要修改指针指向的数据,记得加 const。这既是文档,也是保护。
指针和函数的结合,是C语言从“会用”到“会设计”的分水岭。掌握了这些,你就能写出更灵活、更模块化的代码。嗯,今天就到这里,多写多练,这些概念自然会变成你的肌肉记忆。