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);
}

这个框架你看懂了吗?参数用结构体打包,错误码用枚举,回调函数让用户自定义,函数指针实现解耦。嗯,这就是函数复用的精髓。

记住一句话:好的函数设计,是让调用者只关心「做什么」,不关心「怎么做」。