20. 结构体与设计模式:单例模式、工厂模式在C语言中的实现

说到设计模式,很多人第一反应是Java、C++这些面向对象语言。其实C语言也能用,而且用好了非常顺手。我这些年做嵌入式项目,发现结构体是实现设计模式的天然载体。今天咱们就聊聊两个最常用的:单例模式和工厂模式。

20.1 单例模式:全局唯一的“大佬”

单例模式,说白了就是一个类只能有一个实例。在嵌入式里,这种场景太多了——系统时钟、硬件抽象层、日志管理器,这些玩意儿你想想看,如果搞出两个实例来,系统不乱套才怪。

20.1.1 经典实现:静态局部变量

我个人习惯用静态局部变量来实现单例。这种方式简单、线程安全(C11之前),而且不用操心内存释放。

// logger.h
typedef struct {
    void (*log)(const char* msg);
    void (*set_level)(int level);
    int level;
} Logger;

Logger* logger_instance(void);

// logger.c
#include "logger.h"
#include <stdio.h>

Logger* logger_instance(void) {
    static Logger instance = {
        .log = log_message,
        .set_level = set_log_level,
        .level = 3
    };
    return &instance;
}

static void log_message(const char* msg) {
    Logger* self = logger_instance();
    if (self->level >= 3) {
        printf("[LOG] %s\n", msg);
    }
}

static void set_log_level(int level) {
    Logger* self = logger_instance();
    self->level = level;
}

我的经验:静态局部变量在第一次调用时初始化,之后每次返回同一个地址。我在一个多线程项目里用过这个方案,配合互斥锁,稳得很。

20.1.2 另一种思路:全局变量 + 初始化标志

有些场景下,你可能需要更灵活的控制。比如我想在系统启动时明确初始化单例,而不是等到第一次调用。这时候可以用全局变量加标志位。

// 单例模式:显式初始化版本
typedef struct {
    uint32_t (*get_tick)(void);
    void (*delay_ms)(uint32_t ms);
} SystemTimer;

static SystemTimer timer_singleton;
static int initialized = 0;

SystemTimer* system_timer_init(void) {
    if (initialized) {
        return &timer_singleton;  // 防止重复初始化
    }
    timer_singleton.get_tick = hal_get_tick;
    timer_singleton.delay_ms = hal_delay_ms;
    initialized = 1;
    return &timer_singleton;
}

避坑指南:我曾经在一个项目里忘了检查initialized标志,结果初始化函数被调了两次,硬件定时器配置被覆盖,整个系统时钟乱了。嗯,从那以后我每次写单例都会加这个检查。

20.2 工厂模式:按需生产的“流水线”

工厂模式解决的是“创建对象”的问题。在C语言里,我们用结构体加函数指针,就能模拟出工厂的效果。你想想看,如果每个模块都直接new一个对象,耦合度太高了。工厂模式把创建逻辑集中管理,改起来方便。

20.2.1 简单工厂:根据类型创建

嵌入式里最常见的场景是:根据硬件版本创建不同的驱动实例。比如我做过一个项目,同一块板子有A/B两个版本,传感器型号不同,但上层接口一样。

// sensor.h
typedef struct {
    int (*init)(void);
    float (*read_temperature)(void);
    float (*read_humidity)(void);
} Sensor;

typedef enum {
    SENSOR_TYPE_A,
    SENSOR_TYPE_B
} SensorType;

Sensor* sensor_create(SensorType type);

// sensor_factory.c
#include "sensor.h"
#include "sensor_a.h"
#include "sensor_b.h"

Sensor* sensor_create(SensorType type) {
    switch (type) {
        case SENSOR_TYPE_A:
            return sensor_a_new();   // 返回SensorA的实例
        case SENSOR_TYPE_B:
            return sensor_b_new();   // 返回SensorB的实例
        default:
            return NULL;
    }
}

核心思想:工厂函数隐藏了具体创建细节。调用者只需要告诉工厂“我要什么类型”,工厂负责new出合适的对象。这样上层代码完全不用关心底层硬件差异。

20.2.2 工厂模式 + 单例:注册式工厂

有时候我们需要动态注册新的“产品”。比如一个通信协议栈,支持多种编解码器,用户可以在运行时注册自己的编解码器。这时候单例工厂就派上用场了。

// codec_factory.h
typedef struct {
    int (*encode)(const uint8_t* in, uint8_t* out, int len);
    int (*decode)(const uint8_t* in, uint8_t* out, int len);
    const char* name;
} Codec;

typedef struct CodecNode {
    Codec* codec;
    struct CodecNode* next;
} CodecNode;

CodecFactory* codec_factory_instance(void);
int codec_factory_register(Codec* codec);
Codec* codec_factory_find(const char* name);

// codec_factory.c
static CodecNode* head = NULL;

CodecFactory* codec_factory_instance(void) {
    static CodecFactory factory = {
        .register_codec = register_codec_impl,
        .find_codec = find_codec_impl
    };
    return &factory;
}

static int register_codec_impl(Codec* codec) {
    CodecNode* node = malloc(sizeof(CodecNode));
    if (!node) return -1;
    node->codec = codec;
    node->next = head;
    head = node;  // 头插法,简单粗暴
    return 0;
}

我的经验:注册式工厂在插件化架构里特别好用。我曾经做一个物联网网关,支持多种传感器协议,就是用这种模式。每个协议插件只需要调用register函数,主程序完全不用改代码。

20.3 两种模式结合:单例工厂

在实际项目中,单例模式和工厂模式经常一起出现。比如系统里只有一个工厂(单例),这个工厂负责创建各种对象(工厂)。

// 单例工厂:全局唯一的对象创建器
typedef struct {
    Sensor* (*create_sensor)(SensorType type);
    Actuator* (*create_actuator)(ActuatorType type);
    int (*register_driver)(const char* name, void* driver);
} ObjectFactory;

ObjectFactory* object_factory_instance(void) {
    static ObjectFactory factory = {
        .create_sensor = sensor_create,
        .create_actuator = actuator_create,
        .register_driver = driver_register
    };
    return &factory;
}

// 使用示例
void system_init(void) {
    ObjectFactory* factory = object_factory_instance();
    Sensor* temp_sensor = factory->create_sensor(SENSOR_TYPE_A);
    Actuator* fan = factory->create_actuator(ACTUATOR_FAN);
    // ... 初始化完成
}

20.4 设计模式在嵌入式中的注意事项

模式 优点 缺点 适用场景
单例模式 全局唯一,节省内存 多线程需加锁 硬件抽象层、日志、配置管理
工厂模式 解耦创建与使用 增加代码量 多硬件版本、插件化架构
单例工厂 集中管理,统一入口 耦合度略高 系统级对象管理

避坑指南:我曾经在一个资源受限的MCU上用了大量工厂模式,结果发现代码体积暴涨。后来我改用编译期条件编译(#ifdef)来替代运行时工厂,省了不少Flash。所以,设计模式虽好,但别滥用——嵌入式里资源永远是第一位的。

20.5 本章知识体系

下面这张图帮你理清单例模式和工厂模式在C语言中的实现脉络:

C语言结构体与设计模式 单例模式 静态局部变量 全局变量+标志 特点:全局唯一、延迟初始化 工厂模式 简单工厂 注册式工厂 特点:解耦创建、集中管理 单例工厂:两种模式结合 嵌入式注意事项:资源受限、多线程安全、代码体积

说白了,设计模式就是前人踩过的坑总结出来的套路。用结构体实现单例和工厂,在C语言里完全可行,而且非常实用。我建议你在下一个嵌入式项目里试试看——先从日志模块的单例开始,慢慢你就会发现,原来C语言也能写出优雅的架构。


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