【说在前面的话】
如果说指针在一些人心中是导致代码“极其不稳定的奇技淫巧”,那么“函数指针”则是导致代码跑飞和艰涩难懂的罪魁祸首。然而,函数指针的定义和使用其实非可以非常简单——请暂时忘记原本你从课本上所学的知识,让我们来看一种函数指针的正确打开方式。
【正文】
假设有一个目标函数,其函数原型是这样的:
extern bool serial_out(uint8_t chByte);
那么如何定义指向该函数原型的函数指针呢?
步骤1:用typedef定义一个函数原型类型:
typedef bool serial_out_t(uint8_t chByte);
或者省略形参的变量名:
typedef bool serial_out_t(uint8_t);
步骤2:使用新类型按照普通指针的使用方法来使用。
serial_out_t *fnPutChar = NULL;
...
fnPutChar = &serial_out;
如果用传统的方法,上面的代码等效为:
bool (*)(uint8_t) fnPutChar = NULL;
...
fnPutChar = serial_out;
...
if (NULL != fnPutChar) {
//! 调用函数指针所指向的函数
bResult = (*fnPutChar)('H'); //!< 这里的"*"可以省略,但最好保留哦
}
...
需要特别注意:
使用这种方法定义和使用函数指针好处非常明显:
关于最后一点,我们不妨做一个极端一点的例子:
假设有一个函数,其输入参数是一个函数指针,其返回函数也是一个函数指针:
typedef struct task_cb_t task_cb_t;
typedef const char * get_err_string_t(task_cb_t *ptTask);
typedef void on_task_cpl_evt_t(task_cb_t *ptTask);
extern get_err_code_t *run_task(
task_cb_t *ptTask,
on_task_cpl_evt_t *fnTaskCPLEvtHandler);
为了让这个例子显得更为合理,我假想了一个调度器,而run_task就是这个调度器执行用户任务的函数。分析上面的代码容易清晰的获得以下信息:
怎么样,是不是看起来一切都简单自然?那你考虑过,如果要做一个指向run_task的函数指针应该是什么样么?套娃开始:
typedef get_err_code_t *run_task_t(task_cb_t *, on_task_cpl_evt *);
【注意】run_task_t 前面的“*”是 (get_err_code_t *)的一部分。
我们可以用新类型run_task_t定义一个函数指针:
static run_task_t *s_fnDispatcher = NULL;
最后,作为一个挑战,我很怀疑有没有人能不借助typedef的方法,重新写出函数指针 s_fnDispatcher 的定义?
欢迎在评论区留言,写下你的答案。
【后记】
借助typedef,函数指针的使用可以极大的简化。与传统方式不同的是,这里typedef定义的不是函数指针本身,而是一个“函数原型的类型”——借助这一小技巧,我们成功的贯彻了“复杂的事情变简单、简单的事情变可靠”的原则。