第9章:函数指针与回调函数:如何实现一个简单的计算器?

说实话,很多C语言初学者看到「函数指针」这四个字就头大。我当年也一样,觉得这东西不就是把函数地址存起来吗?有啥用?直到我在一个嵌入式项目里,需要根据不同的传感器类型调用不同的数据处理函数——如果不用函数指针,就得写一长串 if-else 或者 switch-case,代码又臭又长。

嗯,今天我们就用「实现一个简单计算器」这个经典例子,把函数指针和回调函数彻底讲明白。

9.1 什么是函数指针?

函数指针,说白了就是一个变量,它存的是函数的入口地址。你可以通过这个指针去调用函数,就像通过普通指针去访问变量一样。

定义方式有点绕,但记住这个模板就行:

// 定义一个函数指针类型
// 返回值类型 (*指针名)(参数列表)
int (*calc)(int, int);

这行代码的意思是:calc 是一个指针,它指向一个函数,这个函数接受两个 int 参数,返回一个 int

小技巧: 我个人习惯用 typedef 给函数指针起个别名,这样写起来清爽很多。比如:
typedef int (*CalcFunc)(int, int);
CalcFunc p = add;  // p 指向 add 函数

9.2 用函数指针实现计算器

我们先写四个基本运算函数:

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div_op(int a, int b) { 
    if (b == 0) {
        printf("错误:除数不能为0!\n");
        return 0;
    }
    return a / b; 
}

然后定义一个函数指针数组,把运算函数放进去:

int (*ops[4])(int, int) = {add, sub, mul, div_op};
char operators[] = {'+', '-', '*', '/'};

调用的时候,根据用户输入的操作符索引,直接通过指针调用:

int a = 10, b = 5;
int op_index = 2;  // 对应乘法
int result = ops[op_index](a, b);
printf("%d %c %d = %d\n", a, operators[op_index], b, result);

你看,没有 if-else,没有 switch-case,代码干净利落。我在项目中用这种方式处理过几十种不同的协议解析函数,维护起来特别方便。

9.3 回调函数:让代码更灵活

回调函数,其实就是把函数指针当作参数传给另一个函数。那个函数在合适的时机「回调」你传进去的函数。

举个例子,我们写一个通用的计算函数:

int calculate(int a, int b, int (*func)(int, int)) {
    return func(a, b);
}

// 使用时:
int result = calculate(10, 5, add);   // 15
result = calculate(10, 5, mul);       // 50

这个 calculate 函数根本不知道具体要做什么运算,它只负责调用你传进来的函数。这就是回调的核心思想——把「做什么」和「怎么做」分离开。

重点理解: 回调函数让代码的「行为」可以被外部注入。你不需要修改 calculate 的代码,就能让它执行任意运算。这就是所谓的「开放封闭原则」——对扩展开放,对修改封闭。

9.4 完整计算器示例

下面是一个完整的控制台计算器程序,用函数指针和回调实现:

#include <stdio.h>

typedef int (*CalcFunc)(int, int);

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div_op(int a, int b) {
    if (b == 0) {
        printf("除数不能为0!\n");
        return 0;
    }
    return a / b;
}

// 回调函数:执行计算
int calculate(int a, int b, CalcFunc func) {
    return func(a, b);
}

int main() {
    CalcFunc funcs[] = {add, sub, mul, div_op};
    char ops[] = {'+', '-', '*', '/'};
    int a, b, choice;

    printf("简单计算器\n");
    printf("1: 加法  2: 减法  3: 乘法  4: 除法\n");
    printf("请选择运算:");
    scanf("%d", &choice);
    printf("输入两个整数:");
    scanf("%d %d", &a, &b);

    if (choice < 1 || choice > 4) {
        printf("无效选择\n");
        return 1;
    }

    int result = calculate(a, b, funcs[choice - 1]);
    printf("%d %c %d = %d\n", a, ops[choice - 1], b, result);

    return 0;
}

9.5 函数指针的核心逻辑图

下面这张图展示了函数指针和回调函数在计算器中的工作流程:

函数指针与回调函数工作流程 用户输入 a, b, 操作符 根据操作符选择函数指针 调用 calculate(a, b, funcPtr) 执行 add/sub/mul/div_op 输出计算结果

9.6 避坑指南

函数指针虽然好用,但有几个坑我踩过,分享给你:

  • 类型必须严格匹配:函数指针的返回值类型和参数类型必须和原函数完全一致,否则编译器会报错。我曾经因为参数类型写成了 float 而实际函数用的是 double,排查了半天。
  • 空指针检查:调用函数指针之前,一定要检查它是否为 NULL。我在一个嵌入式项目中,因为某个回调函数没注册,直接调用导致系统崩溃。
  • 作用域问题:函数指针指向的函数必须已经定义或声明,不能指向一个还没出现的函数。
特别注意: 在嵌入式系统中,函数指针通常存储在代码区(Flash),而不是 RAM。如果你在中断服务函数里频繁调用函数指针,要注意执行时间。我曾经在一个定时器中断里用函数指针做回调,结果因为函数体太大,导致中断响应超时,系统复位。

9.7 什么时候用函数指针?

根据我的经验,以下几种场景特别适合用函数指针:

场景 说明 示例
策略模式 根据条件选择不同的算法 计算器、排序算法选择
回调机制 事件驱动、异步处理 定时器回调、按键中断
状态机 每个状态对应一个处理函数 通信协议解析
插件架构 动态加载功能模块 驱动层接口

说白了,函数指针就是C语言里实现「多态」的一种方式。虽然C没有面向对象的语法,但用函数指针一样能写出灵活、可扩展的代码。

嗯,这一章的内容就到这里。函数指针和回调函数是C语言进阶的重要一步,理解了它,你就能写出更优雅、更模块化的代码。下次写菜单程序或者协议解析的时候,不妨试试用函数指针来重构,你会爱上这种感觉的。


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