代码复用策略:函数复用、模块复用、库的复用、设计模式在 C 语言中的应用

说到代码复用,我脑子里立刻浮现出刚入行时的一个场景。那时候我接手一个通信协议栈项目,前任工程师把两千多行代码塞进一个文件里,所有功能揉成一团。我花了整整两周才理清楚哪里是哪里。后来我重写了一遍,拆成十几个小模块,复用率从几乎为零提升到了百分之六十以上。嗯,这就是我们今天要聊的核心——怎么让代码不再是一次性消耗品。

一、函数复用:最基础的复用单元

函数是 C 语言里最小的复用单元。说白了,就是把一段逻辑包装起来,给它一个名字,然后到处调用。我个人习惯是:如果一个操作出现三次以上,就考虑封装成函数。

核心原则:函数应该只做一件事,并且做好这件事。

举个例子,很多嵌入式项目里需要计算 CRC 校验。你想想看,如果每个通信模块都自己写一遍 CRC 计算,不仅代码冗余,而且容易出错。正确的做法是:

// 好的复用:封装成独立函数
uint16_t crc16_calculate(const uint8_t *data, uint32_t len) {
    uint16_t crc = 0xFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int j = 0; j < 8; j++) {
            if (crc & 0x0001) {
                crc = (crc >> 1) ^ 0xA001;
            } else {
                crc >>= 1;
            }
        }
    }
    return crc;
}

// 调用方只需要关心数据,不关心实现
uint16_t packet_crc = crc16_calculate(packet, packet_len);

我在项目中遇到过一个问题:有人把 CRC 计算直接写在发送函数里,结果接收端也要算一遍,又复制粘贴了一次。后来改协议时,两个地方都要改,漏了一个,调试了整整一天。这就是不复用函数的代价。

函数复用小技巧:参数尽量设计成通用类型。比如用 void* 加长度参数,而不是固定结构体指针。这样函数适用范围更广。

二、模块复用:把相关函数打包

函数复用解决的是单一功能的问题。但实际项目中,我们往往需要一组相关的函数协同工作。这时候就需要模块复用了。

模块复用,说白了就是把一组相关的函数、数据结构、宏定义打包成一个独立的 .c 和 .h 文件对。我习惯把模块设计成「黑盒子」——外部只需要知道接口,不需要知道内部实现。

// timer_module.h - 定时器模块接口
#ifndef TIMER_MODULE_H
#define TIMER_MODULE_H

#include <stdint.h>

typedef void (*timer_callback_t)(void *arg);

int  timer_init(uint32_t tick_ms);
int  timer_register(uint32_t timeout_ms, timer_callback_t cb, void *arg);
void timer_unregister(int timer_id);
void timer_poll(void);  // 在主循环中调用

#endif

你看,这个定时器模块隐藏了硬件定时器的具体实现。换平台时,只需要重写 .c 文件,接口完全不变。我曾经把一个项目从 STM32 移植到 GD32,定时器模块只改了底层寄存器操作,上层代码一行没动。

模块复用的陷阱:不要在一个模块里塞太多功能。我见过有人把「定时器 + 串口 + LED 控制」写成一个模块,结果想复用定时器功能时,不得不把串口和 LED 也一起带进来。模块的粒度要单一。

三、库的复用:静态库与动态库

当模块稳定后,下一步就是编译成库。库的复用是最高效的代码复用形式——你连源码都不需要给别人看,只提供接口和二进制文件。

类型 文件后缀 链接时机 适用场景
静态库 .a (Linux) / .lib (Windows) 编译时 嵌入式、固件、对体积敏感的场景
动态库 .so (Linux) / .dll (Windows) 运行时 桌面应用、插件系统、需要热更新的场景

我个人在嵌入式项目里几乎只用静态库。为什么呢?因为嵌入式系统资源有限,动态库的加载器和运行时链接会占用额外内存。而且很多 RTOS 根本不支持动态加载。

创建静态库其实很简单:

# 编译成目标文件
gcc -c timer_module.c -o timer_module.o

# 打包成静态库
ar rcs libtimer.a timer_module.o

# 使用静态库
gcc main.c -L. -ltimer -o main.elf

我记得有一次,团队里有人把库的接口改了,但没有更新头文件。结果链接时一堆「符号未定义」错误。从那以后,我要求每次发布库时,必须附带接口变更说明和版本号。

库复用的关键:接口稳定比什么都重要。一旦库被多个项目使用,修改接口就要慎之又慎。我建议用版本号管理,大版本号变化表示接口不兼容。

四、设计模式在 C 语言中的应用

说到设计模式,很多人觉得那是 C++ 或 Java 的事。其实不然。C 语言虽然没有类和继承,但我们可以用结构体加函数指针来实现类似的效果。

我常用的几个模式:

  • 回调模式:用函数指针实现事件驱动。比如上面定时器模块里的 timer_callback_t
  • 状态机模式:用结构体封装状态和转移表。适合协议解析、按键处理等场景。
  • 观察者模式:用链表维护回调列表。适合多模块监听同一事件。
  • 单例模式:用静态变量保证只有一个实例。适合硬件驱动、全局配置等。

举个例子,状态机模式在 C 语言里可以这样实现:

// 状态机模式示例
typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_ERROR
} state_t;

typedef struct {
    state_t current_state;
    void (*entry)(void);
    void (*run)(void);
    void (*exit)(void);
} state_machine_t;

// 状态转移表
static const state_machine_t fsm[] = {
    [STATE_IDLE]    = { .entry = idle_entry,  .run = idle_run,  .exit = idle_exit },
    [STATE_RUNNING] = { .entry = run_entry,   .run = run_run,   .exit = run_exit },
    [STATE_ERROR]   = { .entry = error_entry, .run = error_run, .exit = error_exit },
};

void fsm_run(state_t *state) {
    fsm[*state].run();
    // 根据条件转移状态
    if (some_condition) {
        fsm[*state].exit();
        *state = STATE_RUNNING;
        fsm[*state].entry();
    }
}

我在项目中用这个模式写过按键驱动。四个按键,每个按键有单击、双击、长按三种状态。如果用 if-else 写,代码会乱成一团。用状态机模式,每个状态独立处理,后来加新按键时,只需要复制状态机实例,改一下参数就行。

设计模式的使用建议:不要为了用模式而用模式。如果简单的 if-else 能解决问题,就别硬套状态机。模式是工具,不是目的。

五、知识体系总览

下面这张图总结了代码复用的四个层次,以及它们之间的关系。你可以把它当作一个参考框架:

C语言代码复用策略体系 函数复用 单一功能封装 模块复用 相关功能打包 库的复用 二进制分发 设计模式 架构级复用 各层次关键点 • 函数复用:单一职责,参数通用化 • 模块复用:接口稳定,隐藏实现细节 • 库的复用:版本管理,二进制兼容 • 设计模式:回调、状态机、观察者、单例 复用层次与抽象程度 低层次 → 具体实现(函数、模块) 高层次 → 抽象架构(库、设计模式) 我的建议 从函数复用开始,逐步提升抽象层次 不要跳过中间层次直接套设计模式

六、避坑指南与个人经验

做了十几年嵌入式开发,我在代码复用上踩过的坑不少。这里分享几个印象深刻的:

  • 过度抽象:我曾经为了「通用性」,给一个简单的 GPIO 控制写了五层封装。结果后来改需求时,自己都看不懂调用关系了。记住:复用是为了解决问题,不是为了炫技。
  • 忽略耦合:模块之间如果耦合太紧,复用就成了空谈。我见过一个「通用」的日志模块,里面居然直接调用了某个硬件的串口驱动。换平台时,日志模块也得跟着改。
  • 没有文档:库的接口如果不写清楚,别人根本不知道怎么用。我要求团队每个模块必须有 .h 文件注释,说明每个函数的功能、参数、返回值、注意事项。

最重要的原则:复用不是一次性的工作。第一次写代码时,先保证功能正确。等代码稳定了,再考虑提取成可复用的模块。过早优化是万恶之源。

嗯,关于代码复用,今天就聊到这里。记住四个层次:函数、模块、库、设计模式。从底层做起,逐步积累自己的代码库。你会发现,项目越做越快,bug 越来越少。


公众号:蓝海资料掘金营,微信deep3321