4. 函数指针数组:让代码拥有“选择能力”

好,咱们接着聊函数指针。前面我们学了单个函数指针怎么用,但实际项目中,你往往需要管理一组功能相似的函数。这时候,函数指针数组就派上用场了。

说白了,函数指针数组就是一个数组,数组里每个元素都是一个函数指针。它让程序能根据条件,动态选择执行哪个函数。这种能力在状态机、菜单系统里特别常见。

4.1 函数指针数组的定义与使用

先看定义语法。假设我们有三个数学运算函数:

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 (*op_func[])(int, int) = {add, sub, mul};

嗯,这里要注意:op_func 是一个数组,每个元素都是 int (*)(int, int) 类型的函数指针。调用时,直接用下标索引:

int result = op_func[1](10, 5);  // 调用 sub(10, 5),结果是 5

我个人习惯用枚举来定义索引,这样代码可读性更好:

typedef enum {
    OP_ADD,
    OP_SUB,
    OP_MUL,
    OP_MAX
} op_type_t;

int (*op_func[OP_MAX])(int, int) = {add, sub, mul};

// 使用时
op_type_t cmd = OP_ADD;
int result = op_func[cmd](a, b);
小技巧: 我建议把函数指针数组和枚举定义放在一起,这样新增功能时,只需要在枚举里加一项,再在数组里加一个函数指针,不会漏掉。

4.2 实现状态机

状态机是嵌入式开发的老朋友了。传统做法是用 switch-case,但函数多了,代码会变得又臭又长。用函数指针数组,代码会清爽很多。

我曾经在一个通信协议栈项目里,用函数指针数组管理了 20 多个状态。每个状态对应一个处理函数,状态切换就是改变数组下标。来看个简化例子:

// 状态处理函数
void state_idle(void)    { /* 空闲状态处理 */ }
void state_rx(void)      { /* 接收状态处理 */ }
void state_tx(void)      { /* 发送状态处理 */ }
void state_error(void)   { /* 错误状态处理 */ }

// 状态枚举
typedef enum {
    ST_IDLE,
    ST_RX,
    ST_TX,
    ST_ERROR,
    ST_MAX
} state_t;

// 函数指针数组
void (*state_table[ST_MAX])(void) = {
    state_idle,
    state_rx,
    state_tx,
    state_error
};

// 状态机主循环
state_t current_state = ST_IDLE;

while(1) {
    state_table[current_state]();  // 执行当前状态处理函数
    // 根据事件更新 current_state
}

你看,主循环就一行代码。新增状态时,只需要写新的处理函数,然后在枚举和数组里各加一项。不会像 switch-case 那样,改一个地方要检查所有 case。

避坑指南: 我曾经在状态机里忘记初始化数组,结果运行时跳到了野指针上,系统直接崩溃。记住:函数指针数组必须初始化,每个元素都要指向一个有效的函数。

4.3 实现菜单驱动系统

菜单系统是函数指针数组的另一个经典应用。想想看,一个嵌入式设备的菜单,每个菜单项对应一个操作。用函数指针数组,可以轻松实现菜单的嵌套和跳转。

我做过一个手持设备的菜单系统,有 3 级菜单,每级菜单都是一个函数指针数组。结构大概是这样:

// 菜单项处理函数
void menu_main(void)     { /* 主菜单显示 */ }
void menu_setting(void)  { /* 设置菜单显示 */ }
void menu_about(void)    { /* 关于菜单显示 */ }
void menu_exit(void)     { /* 退出菜单 */ }

// 菜单项结构
typedef struct {
    const char *name;           // 菜单名称
    void (*handler)(void);      // 处理函数
    int  sub_menu;              // 子菜单索引,-1 表示无子菜单
} menu_item_t;

// 主菜单
menu_item_t main_menu[] = {
    {"1. 设置",  menu_setting,  1},
    {"2. 关于",  menu_about,   -1},
    {"3. 退出",  menu_exit,    -1},
    {NULL,       NULL,         -1}   // 结束标记
};

// 设置子菜单
menu_item_t setting_menu[] = {
    {"1. 亮度",  menu_brightness, -1},
    {"2. 音量",  menu_volume,    -1},
    {"3. 返回",  menu_main,       0},
    {NULL,       NULL,           -1}
};

菜单导航时,根据用户输入,调用对应的处理函数,同时可以切换到子菜单。核心逻辑就几句话:

void menu_navigate(menu_item_t *menu, int selection) {
    if (menu[selection].handler) {
        menu[selection].handler();  // 执行菜单功能
        if (menu[selection].sub_menu >= 0) {
            // 切换到子菜单
            current_menu = sub_menus[menu[selection].sub_menu];
        }
    }
}
核心思想: 函数指针数组把「数据」和「行为」解耦了。菜单结构只存函数指针,不关心具体实现。想改某个菜单的功能,只需要改对应的函数,菜单结构不用动。

4.4 知识体系图

下面这张图,帮你理清函数指针数组的核心脉络:

函数指针数组知识体系 函数指针数组 定义语法:int (*arr[])(参数) = {func1, func2} 状态机:state_table[current_state]() 菜单系统:menu[selection].handler() 核心优势:数据与行为解耦 扩展性强,新增功能只需加函数和数组项

函数指针数组,说白了就是给代码装了个「选择器」。你想想看,没有它的时候,你要写一堆 if-else 或 switch-case。有了它,一行代码就能搞定选择和执行。代码量少了,可读性高了,维护也方便了。

嗯,这一节就到这里。记住:函数指针数组不是炫技,它是解决实际问题的工具。下次写状态机或菜单系统时,试试这个思路,你会爱上它的简洁。