设计模式在C中的应用:单例模式、工厂模式、观察者模式的C实现

说实话,很多C程序员一听到「设计模式」就觉得这是Java、C++那帮人的事。我早年也这么想,直到在一个嵌入式项目里被代码维护逼疯——嗯,那时候我才意识到,设计模式本质上就是一些成熟的代码组织套路,跟语言无关。

C语言虽然没有class关键字,但结构体+函数指针的组合,足以让我们实现大部分经典模式。今天我就挑三个最常用的——单例、工厂、观察者,聊聊它们在C里的落地方式。

单例模式:全局唯一的「那个家伙」

单例模式说白了就是:整个程序生命周期里,某个类型的对象只存在一个实例。我在项目中遇到过最典型的场景是日志模块、配置管理器、硬件寄存器映射——这些东西你搞出多个副本,反而会出乱子。

核心要点: 控制实例数量,提供全局访问点。

C里实现单例,最直接的方式就是静态局部变量。来看代码:

#include <stdio.h>
#include <stdlib.h>

typedef struct Logger {
    void (*log)(const char* msg);
    int level;
} Logger;

static Logger* instance = NULL;

static void log_console(const char* msg) {
    printf("[LOG] %s\n", msg);
}

Logger* get_logger_instance() {
    if (instance == NULL) {
        instance = (Logger*)malloc(sizeof(Logger));
        if (instance) {
            instance->log = log_console;
            instance->level = 1;
        }
    }
    return instance;
}

void destroy_logger() {
    if (instance) {
        free(instance);
        instance = NULL;
    }
}
我的习惯: 单例的销毁函数一定要提供。嵌入式系统里,有些模块需要在休眠前释放资源,不留销毁接口就是给自己挖坑。

你可能会问:多线程环境下怎么办?加锁呗。但C标准库没有内置的线程安全机制,我一般用平台相关的互斥量(pthread_mutex或Windows的CRITICAL_SECTION)包一层。不过要注意——双重检查锁定在C里不一定安全,因为编译器可能重排指令。我建议直接用加锁版本,性能损失通常可以接受。

工厂模式:把「造东西」的逻辑藏起来

工厂模式解决的是「创建对象」的耦合问题。你想想看,如果代码里到处都是if (type == A) createA() else if (type == B) createB(),那新增一种类型就得改一堆地方。工厂模式就是把这些判断集中到一个地方。

我在一个通信协议解析项目里用过这个模式。不同的报文类型需要不同的解析器,用工厂模式之后,新增协议类型只需要注册一个创建函数,主流程完全不用动。

C里实现工厂,核心是函数指针数组或注册表:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct Device {
    void (*init)(void);
    void (*send)(const char* data);
} Device;

// 两种设备的具体实现
static void uart_init() { printf("UART init\n"); }
static void uart_send(const char* d) { printf("UART send: %s\n", d); }

static void spi_init() { printf("SPI init\n"); }
static void spi_send(const char* d) { printf("SPI send: %s\n", d); }

// 工厂函数
Device* create_device(const char* type) {
    Device* dev = (Device*)malloc(sizeof(Device));
    if (!dev) return NULL;

    if (strcmp(type, "uart") == 0) {
        dev->init = uart_init;
        dev->send = uart_send;
    } else if (strcmp(type, "spi") == 0) {
        dev->init = spi_init;
        dev->send = spi_send;
    } else {
        free(dev);
        return NULL;
    }
    return dev;
}
注意: 工厂返回的指针,调用方记得要释放。我曾经在一个项目里忘了释放工厂创建的对象,内存泄漏查了两天才定位到。建议配套一个destroy_device函数。

如果类型特别多,if-else链会变得很长。这时候可以用注册表模式——把类型名和创建函数指针存在一个表里,用哈希或二分查找。这样新增类型只需要往表里加一条记录,代码更干净。

观察者模式:一喊多应的事件机制

观察者模式,也叫发布-订阅模式。一个对象状态变了,通知所有依赖它的对象。GUI里的按钮点击、传感器数据更新、状态机状态切换——这些场景都适合用观察者。

我记得在一个智能家居项目里,温度传感器每秒钟上报一次数据。需要更新显示、检查是否超温、记录日志。如果用轮询方式,每个模块都得自己去读传感器,耦合太紧。后来改成观察者模式:传感器作为主题,显示模块、报警模块、日志模块作为观察者注册进去。传感器一有数据,自动通知所有观察者。

C里的实现思路:主题维护一个观察者链表,每个观察者有一个回调函数指针。

#include <stdio.h>
#include <stdlib.h>

#define MAX_OBSERVERS 10

typedef void (*ObserverCallback)(int data);

typedef struct {
    ObserverCallback callbacks[MAX_OBSERVERS];
    int count;
} Subject;

void subject_init(Subject* sub) {
    sub->count = 0;
}

int subject_attach(Subject* sub, ObserverCallback cb) {
    if (sub->count >= MAX_OBSERVERS) return -1;
    sub->callbacks[sub->count++] = cb;
    return 0;
}

void subject_notify(Subject* sub, int data) {
    for (int i = 0; i < sub->count; i++) {
        if (sub->callbacks[i]) {
            sub->callbacks[i](data);
        }
    }
}

// 观察者示例
void display_update(int val) { printf("Display: %d\n", val); }
void alarm_check(int val) {
    if (val > 50) printf("ALARM! Temp too high: %d\n", val);
}

int main() {
    Subject temp_sensor;
    subject_init(&temp_sensor);
    subject_attach(&temp_sensor, display_update);
    subject_attach(&temp_sensor, alarm_check);

    subject_notify(&temp_sensor, 45);
    subject_notify(&temp_sensor, 55);
    return 0;
}
避坑指南: 我曾经在通知函数里直接释放了观察者,结果遍历链表时访问了野指针。建议:如果允许观察者动态增删,通知时先拷贝一份回调列表,或者用标记-清理的方式延迟删除。

观察者模式还有一个变种:带优先级。有些观察者需要先处理(比如紧急报警),有些可以后处理(比如日志记录)。实现上就是在插入时按优先级排序,通知时按顺序调用。

三种模式对比

模式 核心思想 C实现关键 典型场景
单例模式 一个类只有一个实例 静态局部变量 + 访问函数 日志、配置、硬件寄存器
工厂模式 将对象创建逻辑封装 函数指针数组 / 注册表 协议解析、设备驱动
观察者模式 一对多的依赖通知 回调函数链表 事件驱动、传感器数据

知识体系图

下面这张图展示了三种模式在C语言中的定位和关系:

C语言设计模式核心体系 单例模式 全局唯一实例 静态局部变量 线程安全加锁 延迟初始化 日志 / 配置 / 资源池 工厂模式 创建逻辑封装 函数指针注册表 类型-创建函数映射 解耦调用方与实现 设备驱动 / 协议解析 观察者模式 一对多通知 回调函数链表 注册 / 注销 / 通知 事件驱动机制 传感器 / GUI / 状态机 共通基础:结构体 + 函数指针 + 内存管理

这三种模式在C里实现,说白了就是结构体+函数指针的灵活运用。单例管「唯一」,工厂管「创建」,观察者管「通知」。它们不是银弹,但用对了地方,代码的可维护性和可扩展性会明显提升。

我个人建议:别为了用模式而用模式。如果一个模块只有两三个函数,硬套单例反而多余。模式是工具,不是教条。你在实际项目中遇到重复出现的结构问题时,自然就会想到它们。

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