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语言也能写出优雅的架构。
公众号:蓝海资料掘金营,微信deep3321