08 函数指针:写死的函数怎么换

C 语言 OOP 系列导航

  1. C语言你真的会封装吗?
  2. 三个LED写了三份代码?
  3. 同事改一行,LED全乱了
  4. 手搓class:前缀、init/deinit和生命周期
  5. 你的全局变量,该死了
  6. 工业库也是同一套封装套路
  7. struct嵌套消灭复制粘贴
  8. 写死的函数怎么换 ⇐ 当前位置
  9. 把一组函数指针装进对象
  10. 一个指针管所有LED
  11. 虚函数不实现会怎样
  12. 换硬件不改应用
  13. 从自动注册到Linux内核OOP全貌

继承复用了公共字段,但还没有解决行为差异。GPIO LED 的开灯是写电平,PWM LED 的开灯是设占空比,RGB LED 可能要同时驱动三个通道。它们都叫”开灯”,但具体动作不同。

如果把动作写死:

1
2
3
4
void led_run_once(LedBase_t *led)
{
led_base_on(led);
}

那这个函数只能执行固定动作。想支持关灯、翻转、闪烁,就要继续加函数。更好的方向是:对象不变,动作由调用方传进来。这就是函数指针的用处。

函数名本身可以表示函数地址

先定义几个动作:

1
2
3
int led_base_on(LedBase_t *me);
int led_base_off(LedBase_t *me);
int led_base_toggle(LedBase_t *me);

它们都有相同的函数类型 — int (*)(LedBase_t *me)。这个写法第一次看会很别扭,所以通常用 typedef 起别名:

1
typedef int (*LedAction_t)(LedBase_t *me);

LedAction_t 的意思是:指向一个函数,这个函数接收 LedBase_t *,返回 int

把动作当参数传

通用执行函数可以接收一个动作指针:

1
2
3
4
5
6
7
8
9
10
11
12
int led_run_action(LedBase_t *me, LedAction_t action)
{
if (me == NULL) {
return -1;
}

if (action == NULL) {
return -2;
}

return action(me);
}

调用时传入不同函数:

1
2
3
led_run_action(&red.base, led_base_on);
led_run_action(&red.base, led_base_off);
led_run_action(&red.base, led_base_toggle);

函数名加不加括号的区别:

写法 含义
led_base_on 函数地址
led_base_on(&red.base) 立即调用函数
LedAction_t action = led_base_on; 保存函数地址
action(&red.base); 通过函数指针调用

运行时再决定调用谁

函数指针可以把”调用谁”推迟到运行时:

1
2
3
4
5
6
7
8
9
10
11
LedAction_t action = NULL;

if (mode == 0) {
action = led_base_on;
} else if (mode == 1) {
action = led_base_off;
} else {
action = led_base_toggle;
}

led_run_action(&red.base, action);

从固定调用走向可替换行为。

回调:嵌入式最常见的函数指针形态

按钮模块不应该依赖 LED 模块。它只需要在检测到按下时,调用使用者注册的函数:

1
2
3
4
5
6
7
typedef void (*ButtonCallback_t)(void *context);

typedef struct {
uint8_t pin;
ButtonCallback_t on_pressed;
void *context;
} Button_t;

初始化时把回调和用户数据存进去,扫描时触发:

1
2
3
4
5
6
7
8
9
10
void button_scan(Button_t *me)
{
if (me == NULL) {
return;
}

if (gpio_read(me->pin) && me->on_pressed != NULL) {
me->on_pressed(me->context);
}
}

业务层把 LED 操作绑定到按钮事件:

1
2
3
4
5
6
7
static void user_led_toggle(void *context)
{
LedBase_t *led = (LedBase_t *)context;
led_base_toggle(led);
}

button_init(&button, 1, user_led_toggle, &led);

按钮模块不知道 LED 的存在,LED 模块也不知道按钮的存在。两者通过函数指针连接 — 这就是解耦。

函数指针的风险

风险 防御方式
指针为 NULL 调用前检查
函数签名不匹配 typedef 固定类型
context 类型转错 明确约定传入对象类型
回调里做太多事 中断场景下保持短小

函数指针解决的是”调用谁”。至于什么时候调用、回调里能不能阻塞,仍然要按嵌入式场景判断。(C++ 里有人用 std::function 和 lambda 做类似的事,但 C 语言一个 typedef 加指针就够了。)

新人常见误区

误区 更稳的理解
函数指针就是高级语法 它主要解决模块解耦和运行时选择
函数名和函数调用差不多 不加括号是地址,加括号才是调用
void *context 可以随便转 必须由接口约定清楚真实类型
回调里直接做复杂业务 中断或定时器回调应尽量短

嵌入式里到处都是

按键、定时器、通信协议、驱动适配、状态机 — 这些场景都在用函数指针。它是从”写死调用”走向”可替换行为”的第一步。

函数指针的本质:把行为当成数据保存和传递。函数名不加括号是地址,加括号才是调用。