作者:GorgonMeducer 傻孩子
首发:裸机思维
【说在前面的话】
如果说指针在一些人心中是导致代码“极其不稳定的奇技淫巧”,那么“函数指针”则是导致代码跑飞和艰涩难懂的罪魁祸首。然而,函数指针的定义和使用其实非可以非常简单——请暂时忘记原本你从课本上所学的知识,让我们来看一种函数指针的正确打开方式。
【正文】
假设有一个目标函数,其函数原型是这样的:
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;
如果用传统的方法,上面的代码等效为:
bool (*)(uint8_t) fnPutChar = NULL;
- 使用函数指针的来访问函数
...
需要特别注意:
- 我们并不是通过typedef来直接定义指针类型,而是定义一个专门针对目标函数原型的新类型——这样在定义函数指针变量时就和普通变量类型一样需要使用“*”——任何时候都知道这是一个指针,不会迷惑。
- 虽然这里"&"在C语言语法上是可以省略的,但是为了简化规则(简化需要记忆的特殊情况),这里我们要遵守普通指针的使用规则——取地址的时候要使用取地址运算符“&”,访问指针所指向空间的时候,“*”也不能省略。
使用这种方法定义和使用函数指针好处非常明显:
- 极大的提高了代码的可读性——与函数指针有关的代码,任何时候一眼看就知道是一个指针;
- 极大的降低了函数指针的使用难度——通过typedef定义一个针对函数原型的类型,将函数指针的使用变得跟普通指针一摸一样,从而省区了额外的记忆负担;
- 允许轻松套娃
关于最后一点,我们不妨做一个极端一点的例子:
假设有一个函数,其输入参数是一个函数指针,其返回函数也是一个函数指针:
typedef struct task_cb_t task_cb_t;
为了让这个例子显得更为合理,我假想了一个调度器,而run\_task就是这个调度器执行用户任务的函数。分析上面的代码容易清晰的获得以下信息:
- task\_cb\_t 是用户任务的控制块,具体内容未知,但我们可以用它来声明指针变量;
- 函数指针(get\_err\_code\_t *)指向的函数可以返回指定任务的错误代码;
- 函数指针(on\_task\_cpl\_evt\_t *)所指向的函数是一个事件处理程序;
- 函数 run\_task会执行指定的任务,“可能”会在任务执行完成的时候通过函数指针 fnTaskCPLEvtHandler调用一个用户指定的事件处理程序;
- 函数run\_task在执行指定任务的时候,如果发生了错误,“可能”会返回一个非NULL的函数指针,类型是:(get\_err\_code\_t *),用户可以通过这个函数指针获取任务ptTask专属的错误信息(字符串);
怎么样,是不是看起来一切都简单自然?那你考虑过,如果要做一个指向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定义的不是函数指针本身,而是一个“函数原型的类型”——借助这一小技巧,我们成功的贯彻了“复杂的事情变简单、简单的事情变可靠”的原则。
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。