3、函数复用技巧:函数参数设计、返回值处理、回调函数机制、函数指针应用
函数复用,说白了就是让一段逻辑能在不同场景下反复使用。我见过不少新手,写一个函数恨不得把所有功能都塞进去,结果下次换个需求就得重写。嗯,这其实是个设计问题。
今天咱们就聊聊,怎么把函数设计得既灵活又稳定。我个人习惯从四个角度入手:参数怎么传、返回值怎么处理、回调函数怎么用、函数指针怎么玩。
3.1 函数参数设计:少即是多
参数设计是函数复用的第一道坎。你想想看,一个函数如果参数太多,调用者记不住,维护者看不懂,复用起来更是噩梦。
核心原则:参数越少,复用性越强。
我建议遵循以下几点:
- 参数数量控制在 4 个以内。超过 4 个,就该考虑用结构体打包了。
- 避免使用全局变量传参。全局变量会让函数产生隐式依赖,换个环境就崩。
- 输入参数用 const 修饰。告诉调用者:你放心,我不会改你的数据。
- 输出参数用指针。但要注意,指针不能是 NULL。
经验之谈:我在项目中遇到过,一个驱动初始化函数有 12 个参数,每次调用都要翻手册查顺序。后来我改成用结构体传参,代码瞬间清爽了。
来看个对比:
// 糟糕的设计:参数太多,顺序容易搞错
void uart_init(uint32_t baud, uint8_t data_bits, uint8_t stop_bits,
uint8_t parity, uint8_t flow_ctrl, uint8_t mode);
// 好的设计:用结构体打包
typedef struct {
uint32_t baud;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity;
uint8_t flow_ctrl;
uint8_t mode;
} uart_config_t;
void uart_init(const uart_config_t *config);
小技巧:结构体传参时,记得用 const 指针。这样既能传大量参数,又能保证数据不被意外修改。
3.2 返回值处理:别让错误静默
返回值是函数和调用者之间的契约。我见过太多代码,函数返回了错误码,调用者却直接忽略。结果就是:bug 藏得深,排查累死人。
核心原则:每个返回值都要被检查。
我个人习惯用枚举定义错误码:
typedef enum {
ERR_OK = 0,
ERR_PARAM = -1,
ERR_TIMEOUT = -2,
ERR_BUSY = -3,
ERR_UNKNOWN = -4
} err_code_t;
为什么用枚举?因为可读性强。你看到 ERR_TIMEOUT 就知道是超时了,比看数字 0xFFFFFFF 舒服多了。
注意:千万不要把错误码和有效返回值混在一起。比如函数返回 -1 表示错误,但 -1 也可能是有效数据。这种设计会坑死维护者。
我曾经接手过一个项目,某个函数返回 0 表示成功,返回正数表示数据长度,返回负数表示错误。结果呢?调用者写了一大堆 if-else 来判断,代码又臭又长。
我的建议:
- 成功时返回 0 或正数
- 失败时返回负数错误码
- 用
int类型,别用void掩盖错误
3.3 回调函数机制:把控制权交给调用者
回调函数,说白了就是「你告诉我怎么做,我来决定什么时候做」。这是实现解耦的利器。
举个例子,你写了一个定时器模块,想让用户自定义超时后的处理逻辑。这时候回调函数就派上用场了:
typedef void (*timer_callback_t)(void *arg);
typedef struct {
uint32_t period;
timer_callback_t callback;
void *arg;
} timer_t;
void timer_start(timer_t *tmr) {
// 启动定时器...
// 超时后调用:
if (tmr->callback) {
tmr->callback(tmr->arg);
}
}
你看,定时器模块不需要知道用户要做什么,它只负责在正确的时间调用回调函数。这就是解耦。
避坑指南:我曾经在回调函数里直接操作硬件寄存器,结果导致中断冲突。后来我学乖了:回调函数里只做逻辑处理,不做硬件操作。
使用回调函数的几个要点:
- 回调函数要快。别在回调里做耗时操作,否则会阻塞系统。
- 检查回调指针是否为 NULL。调用前一定要判空。
- 提供 void * 参数。让用户能传入自定义数据。
3.4 函数指针应用:灵活调度的艺术
函数指针,说白了就是「把函数当成变量来用」。这在实现状态机、驱动层抽象、插件机制时特别有用。
我举个例子,假设你要实现一个按键驱动,支持长按、短按、双击。用函数指针可以这样设计:
typedef struct {
void (*short_press)(void);
void (*long_press)(void);
void (*double_click)(void);
} key_handler_t;
static key_handler_t g_key_handler;
void key_register_handler(const key_handler_t *handler) {
g_key_handler.short_press = handler->short_press;
g_key_handler.long_press = handler->long_press;
g_key_handler.double_click = handler->double_click;
}
// 按键检测到事件时:
void key_process(key_event_t event) {
switch (event) {
case KEY_SHORT_PRESS:
if (g_key_handler.short_press)
g_key_handler.short_press();
break;
case KEY_LONG_PRESS:
if (g_key_handler.long_press)
g_key_handler.long_press();
break;
// ...
}
}
这样设计的好处是:按键驱动和业务逻辑完全解耦。你想改短按的逻辑?只需要换一个回调函数就行,驱动代码一行都不用动。
我的习惯:函数指针的声明一定要用 typedef,不然代码会变得很难看。你看看 void (*func)(int) 和 func_t func,哪个更清爽?
函数指针的常见应用场景:
| 场景 | 说明 | 示例 |
|---|---|---|
| 状态机 | 每个状态对应一个处理函数 | state_table[state]() |
| 驱动抽象层 | 同一接口支持不同硬件 | spi->read() |
| 插件机制 | 运行时动态加载功能 | plugin->init() |
| 回调通知 | 异步事件处理 | timer->callback() |
注意:函数指针用多了,代码的调用链会变得不直观。调试时你可能找不到到底调了哪个函数。我的建议是:只在需要解耦的地方用,别滥用。
3.5 综合案例:一个可复用的按键驱动
最后,咱们把上面的技巧串起来,写一个完整的按键驱动框架。这个框架支持:
- 多种按键事件(按下、释放、长按)
- 用户自定义回调
- 参数可配置
// key_driver.h
typedef enum {
KEY_EVENT_PRESS,
KEY_EVENT_RELEASE,
KEY_EVENT_LONG_PRESS
} key_event_t;
typedef void (*key_callback_t)(key_event_t event, void *arg);
typedef struct {
uint8_t pin;
uint32_t long_press_ms;
key_callback_t callback;
void *arg;
} key_t;
err_code_t key_init(key_t *key, const key_config_t *cfg);
void key_scan(key_t *key);
// key_driver.c
err_code_t key_init(key_t *key, const key_config_t *cfg) {
if (!key || !cfg) return ERR_PARAM;
key->pin = cfg->pin;
key->long_press_ms = cfg->long_press_ms;
key->callback = cfg->callback;
key->arg = cfg->arg;
// 初始化 GPIO...
return ERR_OK;
}
void key_scan(key_t *key) {
if (!key || !key->callback) return;
// 检测按键状态...
// 触发事件时:
key->callback(event, key->arg);
}
这个框架你看懂了吗?参数用结构体打包,错误码用枚举,回调函数让用户自定义,函数指针实现解耦。嗯,这就是函数复用的精髓。
记住一句话:好的函数设计,是让调用者只关心「做什么」,不关心「怎么做」。