设计模式入门:1. 单例模式详解 C++实现
设计模式入门1. 单例模式详解 C实现前言什么是设计模式在软件开发的世界里我们经常会遇到一些重复出现的问题。设计模式Design Pattern就是这些问题的经过验证的、通用的解决方案。它们不是具体的代码而是一套解决特定问题的最佳实践和思想。设计模式最早由四人帮GoFGang of Four在1994年的《设计模式可复用面向对象软件的基础》一书中系统地提出共包含23种经典模式分为三大类创建型模式关注对象的创建过程如单例、工厂、建造者等结构型模式关注类和对象的组合结构如适配器、装饰器、代理等行为型模式关注对象间的交互和职责分配如观察者、策略、迭代器等学习设计模式的好处不言而喻提高代码的可复用性和可维护性提供了一套通用的设计语言让开发者之间的沟通更高效帮助我们写出更优雅、更健壮的代码今天我们就从最简单也最常用的单例模式开始我们的设计模式之旅。单例模式概述什么是单例模式单例模式Singleton Pattern是一种创建型设计模式它的核心思想非常简单确保一个类只有一个实例并提供一个全局访问点来获取这个实例。换句话说单例模式保证了在整个程序生命周期内某个类只能被实例化一次所有地方访问到的都是同一个对象。单例模式的应用场景单例模式适用于以下场景资源管理器如日志管理器、配置管理器、数据库连接池全局状态管理如游戏中的游戏管理器、应用程序的主控制器硬件访问如打印机驱动、传感器控制器工具类如数学工具类、字符串工具类不过现代C更推荐使用命名空间单例模式的优缺点优点严格控制对实例的访问避免频繁创建和销毁对象提高性能节省系统资源提供全局统一的访问点缺点违反了单一职责原则一个类既要负责自己的业务逻辑又要负责实例的创建扩展困难因为单例类没有抽象层对测试不友好难以模拟单例对象在多线程环境下需要特别注意线程安全问题C实现单例模式的几种方式在C中实现单例模式有多种方式每种方式都有其优缺点和适用场景。我们将从最简单的版本开始逐步演进到最推荐的版本。1. 饿汉式单例Eager Initialization饿汉式单例是最简单的实现方式它在程序启动时就创建实例不管你用不用。// SingletonEager.hclassSingletonEager{public:// 禁止拷贝和移动SingletonEager(constSingletonEager)delete;SingletonEageroperator(constSingletonEager)delete;SingletonEager(SingletonEager)delete;SingletonEageroperator(SingletonEager)delete;// 全局访问点staticSingletonEagergetInstance(){returninstance;}// 示例业务方法voiddoSomething(){// 业务逻辑}private:// 私有构造函数防止外部实例化SingletonEager(){// 初始化代码}// 静态成员变量程序启动时就创建staticSingletonEager instance;};// SingletonEager.cpp// 在类外初始化静态成员变量SingletonEager SingletonEager::instance;优点实现简单天生线程安全因为静态变量在程序启动时就初始化了缺点程序启动时就创建实例即使从未使用过也会占用内存如果有多个单例类且它们之间有依赖关系可能会出现初始化顺序问题2. 懒汉式单例Lazy Initialization- 基础版懒汉式单例是延迟初始化的只有在第一次调用getInstance()时才创建实例。// SingletonLazyBasic.hclassSingletonLazyBasic{public:// 禁止拷贝和移动SingletonLazyBasic(constSingletonLazyBasic)delete;SingletonLazyBasicoperator(constSingletonLazyBasic)delete;SingletonLazyBasic(SingletonLazyBasic)delete;SingletonLazyBasicoperator(SingletonLazyBasic)delete;// 全局访问点staticSingletonLazyBasic*getInstance(){if(instancenullptr){instancenewSingletonLazyBasic();}returninstance;}// 示例业务方法voiddoSomething(){// 业务逻辑}private:// 私有构造函数SingletonLazyBasic(){// 初始化代码}// 静态指针初始化为nullptrstaticSingletonLazyBasic*instance;};// SingletonLazyBasic.cppSingletonLazyBasic*SingletonLazyBasic::instancenullptr;优点延迟初始化只有在需要时才创建实例节省内存避免了饿汉式的初始化顺序问题缺点线程不安全在多线程环境下如果多个线程同时进入if (instance nullptr)判断可能会创建多个实例3. 线程安全的懒汉式单例 - 双重检查锁DCL为了解决基础版懒汉式的线程安全问题我们可以使用双重检查锁Double-Checked Locking。// SingletonDCL.h#includemutexclassSingletonDCL{public:// 禁止拷贝和移动SingletonDCL(constSingletonDCL)delete;SingletonDCLoperator(constSingletonDCL)delete;SingletonDCL(SingletonDCL)delete;SingletonDCLoperator(SingletonDCL)delete;// 全局访问点staticSingletonDCL*getInstance(){// 第一次检查如果实例已经存在直接返回避免每次都加锁if(instancenullptr){// 加锁保证只有一个线程进入下面的代码块std::lock_guardstd::mutexlock(mutex);// 第二次检查防止多个线程同时通过第一次检查后重复创建实例if(instancenullptr){instancenewSingletonDCL();}}returninstance;}// 示例业务方法voiddoSomething(){// 业务逻辑}private:// 私有构造函数SingletonDCL(){// 初始化代码}// 静态指针和互斥量staticSingletonDCL*instance;staticstd::mutex mutex;};// SingletonDCL.cppSingletonDCL*SingletonDCL::instancenullptr;std::mutex SingletonDCL::mutex;优点线程安全延迟初始化性能较好只有第一次创建实例时才需要加锁注意在C11之前由于编译器的指令重排问题双重检查锁可能会失效。但在C11及以后的标准中new操作符的语义得到了明确双重检查锁是安全的。4. 局部静态变量单例Meyers’ Singleton- 最推荐的方式这是由Scott Meyers提出的一种非常优雅的单例实现方式也是现代C中最推荐的单例实现方式。// SingletonMeyers.hclassSingletonMeyers{public:// 禁止拷贝和移动SingletonMeyers(constSingletonMeyers)delete;SingletonMeyersoperator(constSingletonMeyers)delete;SingletonMeyers(SingletonMeyers)delete;SingletonMeyersoperator(SingletonMeyers)delete;// 全局访问点staticSingletonMeyersgetInstance(){// 局部静态变量第一次调用时初始化staticSingletonMeyers instance;returninstance;}// 示例业务方法voiddoSomething(){// 业务逻辑}private:// 私有构造函数SingletonMeyers(){// 初始化代码}};为什么这是最推荐的方式实现最简单代码量最少最容易理解和维护线程安全在C11及以后的标准中局部静态变量的初始化是线程安全的延迟初始化只有在第一次调用getInstance()时才创建实例没有内存泄漏问题静态变量在程序结束时会自动销毁5. 模板单例如果我们需要多个单例类为每个类都写一遍单例代码会很繁琐。这时我们可以使用模板来实现一个通用的单例基类。// SingletonTemplate.htemplatetypenameTclassSingletonTemplate{public:// 禁止拷贝和移动SingletonTemplate(constSingletonTemplate)delete;SingletonTemplateoperator(constSingletonTemplate)delete;SingletonTemplate(SingletonTemplate)delete;SingletonTemplateoperator(SingletonTemplate)delete;// 全局访问点staticTgetInstance(){staticT instance;returninstance;}protected:// 保护构造函数允许子类继承SingletonTemplate()default;virtual~SingletonTemplate()default;};// 使用示例classMyClass:publicSingletonTemplateMyClass{// 让基类可以访问私有构造函数friendclassSingletonTemplateMyClass;public:voiddoSomething(){// 业务逻辑}private:MyClass(){// 初始化代码}};// 调用方式// MyClass::getInstance().doSomething();优点代码复用避免重复编写单例逻辑所有单例类的实现方式统一缺点子类需要将基类声明为友元稍微有点麻烦单例模式的注意事项和常见陷阱1. 内存泄漏问题在使用指针实现的懒汉式单例中new出来的对象在程序结束时不会自动销毁可能会导致内存泄漏。虽然现代操作系统会在程序结束时回收所有内存但这仍然是一个不好的编程习惯。解决方法使用Meyers’ Singleton局部静态变量它会自动销毁使用智能指针std::unique_ptr来管理实例提供一个destroyInstance()方法在程序结束时手动调用2. 多线程安全问题这是单例模式中最容易出错的地方。在多线程环境下一定要确保单例的创建是线程安全的。永远不要使用基础版的懒汉式单例除非你能保证它只会在单线程环境下使用。3. 序列化和反序列化问题如果单例类需要支持序列化和反序列化那么反序列化时可能会创建新的实例破坏单例的特性。解决方法重写反序列化方法让它返回已有的实例避免让单例类支持序列化4. 反射问题在支持反射的语言如Java、C#中可以通过反射调用私有构造函数来创建新的实例。不过C没有原生的反射机制所以这个问题在C中不常见。总结单例模式是最简单也最常用的设计模式之一它确保一个类只有一个实例并提供全局访问点。在C中**Meyers’ Singleton局部静态变量**是最推荐的实现方式它简单、线程安全、延迟初始化且没有内存泄漏问题。实现方式线程安全延迟初始化实现复杂度推荐指数饿汉式✅❌低⭐⭐⭐基础懒汉式❌✅低⭐双重检查锁✅✅中⭐⭐⭐⭐Meyers’ Singleton✅✅极低⭐⭐⭐⭐⭐模板单例✅✅中⭐⭐⭐⭐最后需要提醒的是单例模式虽然好用但不要滥用。只有当你确实需要确保一个类只有一个实例时才应该使用单例模式。过度使用单例会导致代码耦合度增加难以测试和维护。