设计模式在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语言项目中的定位和关系:

C语言设计模式核心体系 单例模式 工厂模式 观察者模式 实现技术 静态变量 + 访问函数 函数指针表 + 注册 回调函数链表 典型应用场景 配置管理 / 日志 / HAL 驱动管理 / 协议解析 事件驱动 / 消息分发 核心思想:将变化的部分封装起来,让不变的部分稳定

六、实际项目中的组合使用

这三种模式在实际项目中经常组合使用。我给你举个例子:

在一个智能家居网关项目中,我这样设计:

  • 单例模式:管理全局配置和日志系统,确保只有一个配置实例
  • 工厂模式:根据设备类型(温度传感器、灯光控制器、门锁等)动态创建对应的驱动实例
  • 观察者模式:当传感器数据变化时,通知显示模块、存储模块、云端上报模块

这样设计的好处是:

  • 新增设备类型时,只需要注册到工厂,其他模块不用改
  • 新增通知接收方时,只需要订阅事件,发送方不用改
  • 全局配置统一管理,不会出现多个副本不一致的问题

我的建议:不要为了用模式而用模式。如果你的项目只有几百行代码,全局变量就够用了。当代码量超过5000行,或者需要多人协作时,设计模式的价值才会真正体现出来。

嗯,这三种模式在C语言里的实现,说白了就是结构体 + 函数指针 + 静态变量的灵活组合。你不需要记住所有细节,关键是理解它们解决什么问题。下次遇到「全局唯一」「动态创建」「事件通知」这类需求时,能想到用这些模式就够了。

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