创建型模式:单例模式(C语言实现与实战)

C语言及嵌入式开发中,常遇这类问题:日志器多实例致日志错乱、配置管理器多实例致参数不一致、重复创建资源浪费MCU内存算力。这些"资源唯一性"问题,单例模式可完美解决。本文从C语言视角,拆解单例模式原理、工程实现与实战场景,对比饿汉式与懒汉式优劣,附可移植线程安全代码,助力快速落地。

一、原理拆解:单例模式的核心逻辑

单例模式核心思想:确保 "类"( C语言对应"结构体+操作函数") 一个实例提供全局 唯一 访问点。即让资源实例"独一无二",所有模块通过统一接口获取,避免重复创建的资源浪费与数据不一致。

C语言实现单例的核心约束:① 禁止外部直接创建(隐藏结构体构造逻辑,不让外部malloc创建);② 内部保证仅创建一次;③ 提供全局访问函数(如get_instance())供外部获取实例。

单例模式分两种核心实现:饿汉式(程序启动/main前提前初始化实例)、懒汉式(第一次调用访问函数时初始化)。二者实现难度、性能、适用场景差异显著,后续详细对比。

二、工程化分析:C语言实现单例的关键考量

C语言无面向对象"类"与访问控制,需借助语言特性模拟单例约束,同时兼顾嵌入式资源限制与线程安全,核心考量四点:

  1. 实例唯一性:.h文件仅声明结构体指针,不暴露定义;构造逻辑(malloc+初始化)封装在.c文件,外部无法直接创建。

  2. 线程安全:多线程下懒汉式首次创建易出现竞态条件(重复创建),嵌入式RTOS/多任务场景需用原子操作或互斥锁解决。

  3. 资源适配:饿汉式提前占内存,实例过大影响启动;懒汉式按需创建省内存,但首次调用有初始化开销,需按需权衡。

  4. 销毁策略:单例生命周期通常与程序一致,嵌入式一般无需主动销毁;涉及动态资源(文件句柄、缓冲区)需提供销毁函数释放资源。

三、C语言实现:饿汉式+懒汉式(含线程安全优化)

以下实现饿汉式与懒汉式单例,重点解决访问控制与线程安全问题,代码适配嵌入式(ARM+FreeRTOS,线程安全部分可按需调整)。

1. 饿汉式单例实现(简单稳定,适合轻量实例)

饿汉式核心是"提前初始化",利用C语言全局变量启动时(main前)初始化特性,天然保证实例唯一与创建一次。

第一步:.h文件(对外接口,隐藏实现)

c 复制代码
#ifndef SINGLETON_HUNGRY_H
#define SINGLETON_HUNGRY_H

// 不暴露结构体定义,外部仅能使用指针
typedef struct SingletonHungry SingletonHungry;

// 全局访问函数:获取唯一实例
SingletonHungry* hungry_singleton_get_instance(void);

// (可选)资源销毁函数
void hungry_singleton_destroy(void);

#endif // SINGLETON_HUNGRY_H

第二步:.c文件(内部实现,封装构造)

c 复制代码
#include "singleton_hungry.h"
#include <stdlib.h>

// 定义结构体(仅内部可见)
struct SingletonHungry {
    // 单例持有资源,比如配置参数、日志文件句柄
    int config_param;
    FILE* log_fd;
};

// 全局变量:程序启动时初始化,保证唯一
static SingletonHungry* g_singleton_instance = NULL;

// 内部初始化函数(仅内部调用)
static void hungry_singleton_init(void) {
    g_singleton_instance = (SingletonHungry*)malloc(sizeof(SingletonHungry));
    if (g_singleton_instance == NULL) {
        // 嵌入式场景可添加断言或错误处理
        while(1); 
    }
    // 初始化资源
    g_singleton_instance->config_param = 100; // 示例配置
    g_singleton_instance->log_fd = fopen("sys.log", "a");
}

// 全局访问函数
SingletonHungry* hungry_singleton_get_instance(void) {
    // 利用C语言全局变量初始化特性,确保只初始化一次
    if (g_singleton_instance == NULL) {
        hungry_singleton_init();
    }
    return g_singleton_instance;
}

// 销毁函数(可选,嵌入式可省略)
void hungry_singleton_destroy(void) {
    if (g_singleton_instance != NULL) {
        if (g_singleton_instance->log_fd != NULL) {
            fclose(g_singleton_instance->log_fd);
        }
        free(g_singleton_instance);
        g_singleton_instance = NULL;
    }
}

饿汉式特点:实现简单,天然线程安全;缺点是提前占内存,实例大或初始化久会影响启动,适合轻量实例。

2. 懒汉式单例实现(按需创建,线程安全优化)

懒汉式核心是"按需创建",首次调用时初始化,省内存但需解决线程安全。提供两种方案:互斥锁(通用稳定)、原子操作(轻量高效,需编译器支持)。

第一步:.h文件(对外接口)

c 复制代码
#ifndef SINGLETON_LAZY_H
#define SINGLETON_LAZY_H

#include <stdatomic.h> // C11原子操作头文件(需编译器支持)

typedef struct SingletonLazy SingletonLazy;

// 全局访问函数
SingletonLazy* lazy_singleton_get_instance(void);

// (可选)销毁函数
void lazy_singleton_destroy(void);

#endif // SINGLETON_LAZY_H

第二步:.c文件(线程安全实现)

c 复制代码
#include "singleton_lazy.h"
#include <stdlib.h>
#include "FreeRTOS.h"
#include "semphr.h" // FreeRTOS互斥锁头文件

// 定义结构体(内部可见)
struct SingletonLazy {
    int config_param;
    FILE* log_fd;
};

// 单例实例指针(静态全局,内部可见)
static SingletonLazy* g_singleton_instance = NULL;

// 方案1:互斥锁实现线程安全(通用,适配所有RTOS)
static SemaphoreHandle_t g_singleton_mutex = NULL;

// 方案2:原子操作实现线程安全(轻量,需C11及以上编译器)
static atomic_flag g_singleton_created = ATOMIC_FLAG_INIT;

// 内部初始化函数
static void lazy_singleton_init(void) {
    g_singleton_instance = (SingletonLazy*)malloc(sizeof(SingletonLazy));
    if (g_singleton_instance == NULL) {
        while(1);
    }
    g_singleton_instance->config_param = 200;
    g_singleton_instance->log_fd = fopen("sys_lazy.log", "a");
    
    // 初始化互斥锁(仅方案1需要)
    if (g_singleton_mutex == NULL) {
        g_singleton_mutex = xSemaphoreCreateMutex();
    }
}

// 全局访问函数(方案1:互斥锁版本)
SingletonLazy* lazy_singleton_get_instance_mutex(void) {
    // 双重检查锁定(DCL):减少互斥锁竞争开销
    if (g_singleton_instance == NULL) {
        xSemaphoreTake(g_singleton_mutex, portMAX_DELAY); // 获取锁
        if (g_singleton_instance == NULL) {
            lazy_singleton_init(); // 仅第一次调用时初始化
        }
        xSemaphoreGive(g_singleton_mutex); // 释放锁
    }
    return g_singleton_instance;
}

// 全局访问函数(方案2:原子操作版本)
SingletonLazy* lazy_singleton_get_instance_atomic(void) {
    // 原子操作判断是否已创建,无锁竞争
    if (!atomic_flag_test_and_set(&g_singleton_created)) {
        lazy_singleton_init(); // 原子操作保证仅执行一次
    }
    return g_singleton_instance;
}

// 销毁函数
void lazy_singleton_destroy(void) {
    if (g_singleton_instance != NULL) {
        if (g_singleton_instance->log_fd != NULL) {
            fclose(g_singleton_instance->log_fd);
        }
        free(g_singleton_instance);
        g_singleton_instance = NULL;
        
        // 销毁互斥锁(方案1)
        if (g_singleton_mutex != NULL) {
            vSemaphoreDelete(g_singleton_mutex);
            g_singleton_mutex = NULL;
        }
        
        // 重置原子标志(方案2,可选)
        atomic_flag_clear(&g_singleton_created);
    }
}

懒汉式关键:① 双重检查锁定(DCL):减少锁竞争开销;② 原子操作vs互斥锁:原子操作轻量(需C11),互斥锁兼容性好;③ 确保锁/原子标志先初始化。

四、实战验证:系统日志器与配置管理器的单例设计

以下以系统日志器、配置管理器为高频场景,验证单例实战效果,代码可直接移植。

1. 实战场景1:系统日志器(懒汉式实现)

系统日志器需统一写入文件,避免重复打开句柄,适合懒汉式(按需省内存)。

c 复制代码
#include "singleton_lazy.h"
#include <stdio.h>

// 日志写入函数(基于单例实现)
void log_write(const char* module, const char* msg) {
    SingletonLazy* log_instance = lazy_singleton_get_instance_atomic();
    if (log_instance->log_fd != NULL) {
        fprintf(log_instance->log_fd, "[%s] %s\n", module, msg);
        fflush(log_instance->log_fd); // 确保日志即时写入
    }
}

// 测试:多个模块调用日志函数
void module_a(void) {
    log_write("ModuleA", "Init success");
}

void module_b(void) {
    log_write("ModuleB", "Data received");
}

// 主函数测试
int main(void) {
    module_a();
    module_b();
    lazy_singleton_destroy();
    return 0;
}

验证效果:运行后sys_lazy.log中两模块日志有序写入,无错乱,单例唯一有效。

2. 实战场景2:配置管理器(饿汉式实现)

配置管理器需启动时加载配置(如Flash读取),供全局读取,适合饿汉式(提前初始化,启动即用)。

c 复制代码
#include "singleton_hungry.h"
#include <stdio.h>

// 加载配置(从Flash读取,简化示例)
static void load_config(SingletonHungry* instance) {
    // 实际场景中替换为Flash读取逻辑
    instance->config_param = 123; // 模拟从Flash读取的配置
}

// 重写内部初始化函数(添加配置加载)
static void hungry_singleton_init(void) {
    g_singleton_instance = (SingletonHungry*)malloc(sizeof(SingletonHungry));
    if (g_singleton_instance == NULL) {
        while(1);
    }
    load_config(g_singleton_instance); // 启动时加载配置
    g_singleton_instance->log_fd = NULL; // 配置管理器无需日志句柄
}

// 配置读取接口
int config_get_param(void) {
    SingletonHungry* config_instance = hungry_singleton_get_instance();
    return config_instance->config_param;
}

// 测试:多个模块读取配置
void module_c(void) {
    printf("ModuleC config: %d\n", config_get_param());
}

void module_d(void) {
    printf("ModuleD config: %d\n", config_get_param());
}

int main(void) {
    // 程序启动时已完成配置加载
    module_c();
    module_d();
    hungry_singleton_destroy();
    return 0;
}

验证效果:module_c与module_d读取参数均为123,一致且全局共享,单例有效。

五、问题解决:单例模式实现的常见坑与解决方案

C语言实现单例易踩坑,以下总结高频问题与解决方案:

  1. 懒汉式多线程重复创建:未做线程安全保护。解决方案:DCL+互斥锁或C11原子操作。

  2. 外部可直接创建:结构体定义暴露。解决方案:.h仅声明指针,构造逻辑封装在.c。

  3. 饿汉式启动内存不足:实例过大。解决方案:改用懒汉式或优化实例结构减内存。

  4. 销毁后调用崩溃:未重置实例指针。解决方案:销毁时置空指针,懒汉式同步重置原子标志/锁。

  5. DCL失效:编译器指令重排。解决方案:实例指针加volatile关键字禁止优化。

六、饿汉式vs懒汉式:选型总结+互动引导

两种实现核心差异总结(快速选型):

特性 饿汉式 懒汉式
创建时机 启动时(main前) 首次调用时
线程安全 天然安全 需额外实现(锁/原子)
内存占用 启动即占,开销大 按需占用,省内存
实现难度 简单 复杂(需解决线程安全)
适用场景 轻量、启动必加载(配置) 重量级、按需使用(日志)

单例模式是C语言底层开发核心设计模式,解决"资源唯一"与"全局共享"问题,适配嵌入式场景,是日志器、配置管理器的首选方案。

对你有帮助的话,别忘了点赞、收藏!后续将更新工厂模式、建造者模式的C语言实现,关注我获取更多底层干货!实际项目遇问题,欢迎评论区讨论~

相关推荐
一行注释2 小时前
ECharts柱状图横向展示与DataZoom滑动查看实现
开发语言·前端·javascript
Ulyanov2 小时前
Impress.js深度解析
开发语言·前端·javascript·css3·impress.js
烤麻辣烫2 小时前
23种设计模式(新手)-9单例模式
java·开发语言·学习·设计模式·intellij-idea
ytttr8732 小时前
基于MATLAB实现时间序列小波相干性分析
开发语言·matlab
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-设计模式的六大原则
java·开发语言·设计模式
Howrun7773 小时前
C++ 智能指针_详细解释
开发语言
hjjdebug3 小时前
switch-case 语句分析(消灭swich-case方法)
c语言·switch-case
编程大师哥3 小时前
JavaScript DOM
开发语言·javascript·ecmascript
dazzle3 小时前
Python数据结构(四):栈详解
开发语言·数据结构·python