第十二章:指针(三):函数指针、回调函数、指针作为函数参数、指针函数

指针这东西,说到函数这块,才真正开始有意思起来。

前面我们聊了指针和数组、指针和字符串,那都是基本功。今天要讲的这几个概念——函数指针、回调函数、指针作为函数参数、指针函数——才是真正让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语言从“会用”到“会设计”的分水岭。掌握了这些,你就能写出更灵活、更模块化的代码。嗯,今天就到这里,多写多练,这些概念自然会变成你的肌肉记忆。


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