设计模式在C中的应用:单例模式、工厂模式、观察者模式的C实现
说实话,很多C语言开发者听到「设计模式」这四个字,第一反应是:那是Java和C++的东西,跟我C语言有什么关系?
我以前也这么想。直到有一次,我在一个嵌入式项目中接手了一个遗留代码库——全局变量满天飞,模块之间互相调用,改一个地方崩三个功能。那段时间我每天都在「改bug → 引入新bug → 再改」的循环里打转。
后来我意识到:设计模式不是语言的专利,它是解决软件设计问题的通用思路。C语言虽然没有class关键字,但结构体、函数指针、宏定义这些工具,足够我们实现大部分经典模式。
今天我就跟你聊聊三个最实用的模式:单例、工厂、观察者。它们在C语言里怎么落地?有哪些坑?我踩过哪些雷?咱们一个一个说。
一、单例模式:全局唯一实例
单例模式,说白了就是保证某个「对象」在整个程序生命周期里只有一个实例。在C语言里,我们通常用全局静态变量来实现。
我见过很多项目直接用全局变量,结果多线程环境下各种竞态问题。单例模式至少给了你一个统一的访问入口,方便加锁、延迟初始化。
经典实现:懒汉式单例
// singleton.h
#ifndef SINGLETON_H
#define SINGLETON_H
typedef struct {
int id;
char name[64];
// ... 其他数据
} AppConfig;
AppConfig* app_config_get_instance(void);
#endif
// singleton.c
#include "singleton.h"
#include <stdlib.h>
static AppConfig* instance = NULL;
AppConfig* app_config_get_instance(void) {
if (instance == NULL) {
instance = (AppConfig*)malloc(sizeof(AppConfig));
if (instance) {
instance->id = 0;
// 初始化其他字段
}
}
return instance;
}
注意:这个实现不是线程安全的。如果两个线程同时第一次调用,可能创建两个实例。我在一个多线程采集项目中就吃过这个亏——两个线程同时初始化配置,结果配置被覆盖了一半。
线程安全版本(加锁)
// 使用互斥锁
#include <pthread.h>
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
AppConfig* app_config_get_instance_safe(void) {
pthread_mutex_lock(&lock);
if (instance == NULL) {
instance = (AppConfig*)malloc(sizeof(AppConfig));
// 初始化...
}
pthread_mutex_unlock(&lock);
return instance;
}
我的习惯:在嵌入式裸机环境下,我通常用「饿汉式」——在程序启动时就创建好实例。这样避免了运行时加锁的开销,而且初始化顺序可控。
二、工厂模式:解耦对象的创建
工厂模式的核心思想是:把「创建对象」的逻辑从业务代码中抽离出来。在C语言里,我们通常用函数指针和注册表来实现。
你想想看,如果你的代码里到处都是 if (type == A) createA() else if (type == B) createB(),那每新增一种类型,你就要改一堆文件。工厂模式就是解决这个问题的。
简单工厂:用函数指针表
// factory.h
#ifndef FACTORY_H
#define FACTORY_H
typedef struct Device {
void (*init)(struct Device* self);
void (*read)(struct Device* self, uint8_t* buf, int len);
void (*deinit)(struct Device* self);
} Device;
typedef Device* (*device_creator_t)(void);
void factory_register(int type, device_creator_t creator);
Device* factory_create(int type);
#endif
// factory.c
#include "factory.h"
#include <string.h>
#define MAX_TYPES 16
static device_creator_t creators[MAX_TYPES];
static int registered_types = 0;
void factory_register(int type, device_creator_t creator) {
if (type >= 0 && type < MAX_TYPES) {
creators[type] = creator;
}
}
Device* factory_create(int type) {
if (type >= 0 && type < MAX_TYPES && creators[type]) {
return creators[type]();
}
return NULL;
}
这样,新增一个设备类型时,只需要写一个创建函数并注册,不需要改工厂代码。我在一个传感器驱动项目中用了这个模式,后来加了6种新传感器,一行工厂代码都没改。
核心价值:工厂模式把「创建」和「使用」分离。你的业务代码只需要知道类型ID,不需要知道具体怎么创建。这大大降低了模块间的耦合。
三、观察者模式:事件驱动的基石
观察者模式,也叫发布-订阅模式。一个对象(主题)状态变化时,通知所有依赖它的对象(观察者)。在C语言里,我们用回调函数链表来实现。
我记得在一个物联网网关项目中,需要同时处理传感器数据上报、远程控制指令、本地按键事件。如果用轮询,CPU占用率直接飙到80%。后来用观察者模式重构,CPU占用降到15%。
简单实现:回调链表
// observer.h
#ifndef OBSERVER_H
#define OBSERVER_H
typedef void (*event_handler_t)(void* context, int event_id, void* data);
typedef struct ObserverNode {
event_handler_t handler;
void* context;
struct ObserverNode* next;
} ObserverNode;
typedef struct {
ObserverNode* head;
int event_count;
} EventBus;
void event_bus_init(EventBus* bus);
void event_bus_subscribe(EventBus* bus, event_handler_t handler, void* context);
void event_bus_unsubscribe(EventBus* bus, event_handler_t handler, void* context);
void event_bus_publish(EventBus* bus, int event_id, void* data);
#endif
// observer.c
#include "observer.h"
#include <stdlib.h>
void event_bus_init(EventBus* bus) {
bus->head = NULL;
bus->event_count = 0;
}
void event_bus_subscribe(EventBus* bus, event_handler_t handler, void* context) {
ObserverNode* node = (ObserverNode*)malloc(sizeof(ObserverNode));
if (node) {
node->handler = handler;
node->context = context;
node->next = bus->head;
bus->head = node;
bus->event_count++;
}
}
void event_bus_publish(EventBus* bus, int event_id, void* data) {
ObserverNode* cur = bus->head;
while (cur) {
if (cur->handler) {
cur->handler(cur->context, event_id, data);
}
cur = cur->next;
}
}
我曾经踩过的坑:在回调函数里删除自己。如果观察者在处理事件时调用了 unsubscribe,链表结构会变,可能导致遍历出错。解决方案是:用「标记删除」或「延迟删除」——先把节点标记为待删除,遍历完再统一清理。
四、三种模式的对比与选择
| 模式 | 核心目的 | C语言实现关键 | 典型场景 |
|---|---|---|---|
| 单例模式 | 保证全局唯一实例 | 静态变量 + 访问函数 | 配置管理、日志、硬件抽象层 |
| 工厂模式 | 解耦对象创建 | 函数指针表 + 注册机制 | 驱动管理、协议解析、插件系统 |
| 观察者模式 | 一对多通知 | 回调函数链表 | 事件驱动、消息分发、状态监控 |
五、知识体系结构图
下面这张图展示了三种模式在C语言项目中的定位和关系:
六、实际项目中的组合使用
这三种模式在实际项目中经常组合使用。我给你举个例子:
在一个智能家居网关项目中,我这样设计:
- 单例模式:管理全局配置和日志系统,确保只有一个配置实例
- 工厂模式:根据设备类型(温度传感器、灯光控制器、门锁等)动态创建对应的驱动实例
- 观察者模式:当传感器数据变化时,通知显示模块、存储模块、云端上报模块
这样设计的好处是:
- 新增设备类型时,只需要注册到工厂,其他模块不用改
- 新增通知接收方时,只需要订阅事件,发送方不用改
- 全局配置统一管理,不会出现多个副本不一致的问题
我的建议:不要为了用模式而用模式。如果你的项目只有几百行代码,全局变量就够用了。当代码量超过5000行,或者需要多人协作时,设计模式的价值才会真正体现出来。
嗯,这三种模式在C语言里的实现,说白了就是结构体 + 函数指针 + 静态变量的灵活组合。你不需要记住所有细节,关键是理解它们解决什么问题。下次遇到「全局唯一」「动态创建」「事件通知」这类需求时,能想到用这些模式就够了。
公众号:蓝海资料掘金营,微信deep3321