函数指针数组:从基础到命令分发表
2026/8/24大约 4 分钟
C语言函数指针数组:从基础到命令分发表
函数指针数组就是:
数组里的每一个元素,都是一个函数指针。
它最重要的价值是:
通过下标直接选择要执行的函数,替代大量
if-else/switch-case。
一、先看一个函数指针
普通函数:
int add(int a, int b)
{
return a + b;
}定义一个函数指针:
int (*p)(int, int) = add;调用:
int ret = p(10, 20);这里:
p
↓
add()那么如果有很多个函数呢?
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int divi(int a, int b);就可以把它们放进一个数组。
二、从一个函数指针变成函数指针数组
int (*ops[4])(int, int) =
{
add,
sub,
mul,
divi
};可以理解成:
ops[0] → add
ops[1] → sub
ops[2] → mul
ops[3] → divi于是:
int ret = ops[0](10, 5);实际上就是:
add(10, 5);而:
ops[1](10, 5);就是:
sub(10, 5);所以函数指针数组的核心就是:
数组下标
↓
选择函数
↓
直接调用三、为什么需要它?
假设没有函数指针数组:
switch (choice)
{
case 0:
ret = add(a, b);
break;
case 1:
ret = sub(a, b);
break;
case 2:
ret = mul(a, b);
break;
case 3:
ret = divi(a, b);
break;
}每增加一个操作,就要继续修改 switch。
有函数指针数组:
ret = ops[choice](a, b);逻辑直接变成:
choice
↓
ops[choice]
↓
对应函数这就是它最直接的价值。
四、例子:计算器
完整一点:
#include <stdio.h>
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 divi(int a, int b)
{
return b ? a / b : 0;
}
int main(void)
{
int (*ops[4])(int, int) =
{
add,
sub,
mul,
divi
};
int choice = 2;
int result = ops[choice](10, 5);
printf("%d\n", result);
return 0;
}当:
choice = 2;执行:
ops[2](10, 5);就是:
mul(10, 5);五、真正的嵌入式用途:命令分发
这比计算器更重要。
假设串口收到命令:
CMD_READ
CMD_WRITE
CMD_RESET传统写法:
switch (cmd)
{
case CMD_READ:
handle_read(data);
break;
case CMD_WRITE:
handle_write(data);
break;
case CMD_RESET:
handle_reset(data);
break;
}命令越来越多,switch 会越来越长。
于是改成:
typedef void (*cmd_handler)(uint8_t *data);
cmd_handler handlers[256];初始化:
handlers[CMD_READ] = handle_read;
handlers[CMD_WRITE] = handle_write;
handlers[CMD_RESET] = handle_reset;收到命令:
handlers[cmd](payload);例如:
收到 CMD_READ
↓
handlers[CMD_READ]
↓
handle_read()这里函数指针数组实际上就是:
一张“命令号 → 处理函数”的映射表。
六、为什么这种写法特别适合协议解析?
比如协议:
┌──────┬──────┬─────────┐
│ HEAD │ CMD │ PAYLOAD │
└──────┴──────┴─────────┘解析完 CMD 后:
handlers[cmd](payload);直接找到对应处理函数。
于是协议解析流程就很自然:
收到数据
↓
解析帧
↓
得到 cmd
↓
handlers[cmd]
↓
执行对应处理函数这样协议层和具体业务处理函数就分开了。
七、例子:状态机
状态机也很适合函数指针数组。
假设有:
enum State
{
STATE_IDLE,
STATE_RX,
STATE_ERROR
};每个状态对应一个处理函数:
void state_idle(void);
void state_rx(void);
void state_error(void);建立状态表:
void (*state_handlers[])(void) =
{
state_idle,
state_rx,
state_error
};当前状态:
int state = STATE_RX;直接:
state_handlers[state]();就进入 state_rx()。
整个状态机变成:
当前状态
↓
数组下标
↓
对应处理函数
↓
执行相比巨大的:
switch (state)结构更加清晰。
八、例子:菜单系统
例如按键菜单:
0 → 主界面
1 → 设置
2 → 调试
3 → 关于定义:
void menu_main(void);
void menu_setting(void);
void menu_debug(void);
void menu_about(void);建立:
void (*menus[4])(void) =
{
menu_main,
menu_setting,
menu_debug,
menu_about
};用户选择:
int choice = 2;
menus[choice]();直接进入:
menu_debug();九、函数指针数组和前面的“多态”有什么关系?
你之前学过:
Device
↓
ops
↓
函数指针
↓
UART / SPI / I2C函数指针数组也是同一个思想:
把“要执行什么行为”从代码里拿出来,放进一个表。
区别是:
函数指针
→ 一个接口对应一个函数
函数指针数组
→ 一个索引对应一个函数所以:
函数指针数组
↓
下标
↓
选择具体函数本质上就是一种查表分发(dispatch table)。
十、注意数组越界
这种代码:
handlers[cmd](data);有一个前提:
cmd
必须是合法下标例如数组只有:
cmd_handler handlers[10];那么:
cmd = 20;就不能直接:
handlers[20](data); // ❌应该先检查:
if (cmd < 10 && handlers[cmd] != NULL)
{
handlers[cmd](data);
}否则可能直接调用非法地址。
十一、最终理解
函数指针数组最重要的不是语法:
int (*ops[4])(int, int);而是它背后的思想:
普通做法:
条件判断
↓
switch / if
↓
选择函数
函数指针数组:
编号
↓
数组下标
↓
函数地址
↓
直接调用所以它特别适合:
命令分发
状态机
协议解析
菜单系统
驱动操作表一句话记忆:
函数指针数组 = “编号 → 函数”的查表机制,用数据代替大量分支。

