函数指针:把行为作为参数
08 函数指针:把行为作为参数
C 语言 OOP 系列导航
本系列面向会 C 和单片机基础、刚进入企业项目的同学,从
struct封装一路讲到ops、多态和类 Linux 驱动框架。
继承复用了公共字段,但还没有解决行为差异。
GPIO LED 的开灯是写电平,PWM LED 的开灯是设置占空比,RGB LED 的开灯可能要同时驱动三个通道。它们都叫“开灯”,但具体动作不同。
如果把动作写死:
1 | void led_run_once(LedBase_t *led) |
那这个函数只能执行固定动作。想支持关灯、翻转、闪烁,就要继续加函数。
一句话先懂:函数指针让“调用哪个函数”变成一个可以传递、保存、替换的值。
先看问题:对象相同,动作可能不同
硬编码调用会让上层越来越臃肿:
1 | void led_run_on(LedBase_t *led); |
更好的方向是:对象保持不变,动作由调用方传进来。
函数名本身可以表示函数地址
先定义几个动作:
1 | int led_base_on(LedBase_t *me); |
它们都有相同的函数类型:
1 | int (*)(LedBase_t *me) |
这个写法第一次看会很别扭,所以通常用 typedef 起别名:
1 | typedef int (*LedAction_t)(LedBase_t *me); |
LedAction_t 的意思是:指向一个函数,这个函数接收 LedBase_t *,返回 int。
把动作当参数传
通用执行函数可以接收一个动作指针:
1 | int led_run_action(LedBase_t *me, LedAction_t action) |
调用时传入不同函数:
1 | led_run_action(&red.base, led_base_on); |
注意函数名加不加括号的区别:
| 写法 | 含义 |
|---|---|
led_base_on |
函数地址 |
led_base_on(&red.base) |
立即调用函数 |
LedAction_t action = led_base_on; |
保存函数地址 |
action(&red.base); |
通过函数指针调用 |
运行时再决定调用谁
函数指针可以把“调用谁”推迟到运行时:
1 | LedAction_t action = NULL; |
这就是从固定调用走向可替换行为。
回调:嵌入式最常见的函数指针形态
按钮模块不应该依赖 LED 模块。它只需要在检测到按下时,调用使用者注册的函数:
1 | typedef void (*ButtonCallback_t)(void *context); |
初始化时把回调和用户数据存进去:
1 | int button_init(Button_t *me, uint8_t pin, |
扫描时触发回调:
1 | void button_scan(Button_t *me) |
业务层把 LED 操作绑定到按钮事件:
1 | static void user_led_toggle(void *context) |
按钮模块不知道 LED 的存在,LED 模块也不知道按钮的存在。两者通过函数指针连接。
函数指针的风险
| 风险 | 防御方式 |
|---|---|
指针为 NULL |
调用前检查 |
| 函数签名不匹配 | 用 typedef 固定类型 |
context 类型转错 |
明确约定传入对象类型 |
| 回调里做太多事 | 中断场景下保持短小 |
函数指针解决的是“调用谁”。至于什么时候调用、回调里能不能阻塞、能不能访问共享资源,仍然要按嵌入式场景判断。
新人常见误区
| 误区 | 更稳的理解 |
|---|---|
| 函数指针就是高级语法 | 它主要解决模块解耦和运行时选择 |
| 函数名和函数调用差不多 | 不加括号是地址,加括号才是调用 |
void *context 可以随便转 |
必须由接口约定清楚真实类型 |
| 回调里直接做复杂业务 | 中断或定时器回调应尽量短 |
C/C++ 对照
| C 写法 | C++ 写法 |
|---|---|
| 函数指针 | 函数对象 / lambda |
typedef int (*Action)(LedBase_t *) |
std::function<int(LedBase*)> |
action(me) |
action(me) |
| 回调函数 | callback |
void *context |
捕获变量 / 用户数据 |
嵌入式项目意义
函数指针在嵌入式里非常常见:
| 场景 | 用法 |
|---|---|
| 按键 | 按下后调用回调 |
| 定时器 | 超时后调用处理函数 |
| 通信协议 | 收到命令后调用 handler |
| 驱动适配 | 不同硬件绑定不同函数 |
| 状态机 | 状态对应处理函数 |
它是从“写死调用”走向“可替换行为”的第一步。
总结
函数指针的本质,是把行为当成数据保存和传递;函数名不加括号是地址,加括号才是调用。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Rvosy的小破站!









