第七章 单件模式

第七章 单件模式

一、单件模式基础认知

  1. 模式别称:单例模式、单态模式
  2. 模式类型:创建型设计模式
  3. 核心目标:保证一个类在整个程序中仅有一个对象实例,同时提供对该实例的全局访问方法
  4. 设计原则:遵循 Scott Meyers 的理念 ——要使接口 / 类型易于正确使用,难以错误使用,将 “保证单实例” 的责任交给类设计者,而非类使用者(避免使用者随意创建多实例引发逻辑混乱 / 性能问题)
  5. 核心实现手段:通过私有构造函数限制类外实例化,配合静态成员和静态方法实现单实例的全局访问

二、核心知识点拆解(结合代码迭代)

(一)单件类的基本概念和实现

1.业务场景

以游戏配置类GameConfig为例,游戏中声音大小、图像质量等配置需全局统一管理,若允许创建多个GameConfig对象,会导致配置不一致、资源浪费等问题。

2.普通类的缺陷

普通GameConfig类可随意创建多实例,代码示例(不可用,仅作对比):

// 普通类的问题:可创建多个实例
_nmsp1::GameConfig g_config1; 
_nmsp1::GameConfig g_config2;
_nmsp1::GameConfig* g_gct = new _nmsp1::GameConfig;

3.基础单件类实现(_nmsp1 基础懒汉式雏形)

核心改造点是私有化实例相关方法、增加静态控制成员,代码如下:

namespace _nmsp1
{
    class GameConfig
    {
    private:
        // 1. 私有化构造、拷贝构造、赋值重载、析构,禁止类外实例化和拷贝
        GameConfig() {}; 
        GameConfig(const GameConfig& tmpobj);
        GameConfig& operator=(const GameConfig& tmpobj);
        ~GameConfig() {}

        // 2. 静态成员指针:指向本类唯一实例
        static GameConfig* m_instance; 

    public:
        // 3. 全局访问接口:获取唯一实例
        static GameConfig* getInstance()
        {
            if (m_instance == nullptr)
            {
                m_instance = new GameConfig();
            }
            return m_instance;
        }
    };
    // 类外初始化静态成员:初始为空指针
    GameConfig* GameConfig::m_instance = nullptr; 
}

4.基础版本的使用方式

_nmsp1::GameConfig* g_gc = _nmsp1::GameConfig::getInstance();
_nmsp1::GameConfig* g_gc2 = _nmsp1::GameConfig::getInstance();
// g_gc和g_gc2指向同一个实例,实现单例

(二)单件类在多线程中可能导致的问题及解决

  1. 多线程安全隐患

    当多个线程同时调用getInstance()时,若线程 1 执行完if (m_instance == nullptr)后被系统切走,线程 2 会进入分支创建实例;线程 1 恢复执行后会再次创建实例,导致多实例,违背单例原则。

  2. 解决方案迭代

    方案 代码实现 优缺点
    粗暴加锁 getInstance()开头加std::lock_guard<std::mutex> gcguard(my_mutex); 优点:解决多线程问题;缺点:每次调用都加锁,频繁调用时效率极低,且对象创建后加锁无意义
    双重锁定(双重检查) if (m_instance == nullptr)内加锁,且加锁后再次判断空 优点:仅对象未创建时加锁,提升效率;缺点:存在内存访问重排序问题(编译器指令重排,可能返回未初始化的实例指针)
    C++11 原子操作 + 内存栅栏(_nmsp2 版本) std::atomic修饰m_instance,配合内存栅栏禁止指令重排 优点:彻底解决双重锁定的重排序问题;缺点:代码复杂度高,需引入<atomic>头文件
    最优简易方案 main函数创建任何线程前,先调用一次GameConfig::getInstance() 优点:提前初始化实例,后续多线程调用无需加锁,简单高效且无性能损耗
  3. 原子操作版本核心代码(_nmsp2)

namespace _nmsp2
{
    class GameConfig
    {
    private:
        GameConfig() {};
        GameConfig(const GameConfig& tmpobj);
        GameConfig& operator = (const GameConfig& tmpobj);
        ~GameConfig() {};

        static atomic<GameConfig*> m_instance; // 原子类型成员
        static std::mutex m_mutex;

    public:
        static GameConfig* getInstance()
        {
            GameConfig* tmp = m_instance.load(std::memory_order_relaxed);
            std::atomic_thread_fence(std::memory_order_acquire); // 禁止指令重排
            if (tmp == nullptr)
            {
                std::lock_guard<std::mutex> lock(m_mutex);
                tmp = m_instance.load(std::memory_order_relaxed);
                if (tmp == nullptr)
                {
                    tmp = new GameConfig();
                    std::atomic_thread_fence(std::memory_order_release);
                    m_instance.store(tmp, std::memory_order_relaxed);
                }
            }
            return tmp;
        }
    };
    std::atomic<GameConfig*> GameConfig::m_instance;
    std::mutex GameConfig::m_mutex;
}

(三)饿汉式与懒汉式

单件模式分为饿汉式懒汉式两种核心实现风格,二者的核心差异是实例创建时机

1.懒汉式(_nmsp1 核心版本)

  • 创建时机:程序启动后不创建实例,第一次调用getInstance()时才初始化
  • 优点:延迟加载,若程序全程未使用该实例,可节省内存资源
  • 缺点:存在多线程安全隐患,需额外处理线程同步
  • 核心代码m_instance初始化为nullptrgetInstance()内判断空后new

2.饿汉式(_nmsp3 核心版本)

  • 创建时机:程序启动时(静态成员初始化阶段)就创建实例,无需等待getInstance()调用

  • 优点:天然线程安全(静态成员初始化早于线程创建),无需处理多线程同步

  • 缺点:过早加载,若实例较大且未使用,会造成内存浪费;多cpp文件中全局变量与静态成员的初始化顺序不确定,可能导致访问未初始化实例

  • 核心代码

    namespace _nmsp3
    {
        class GameConfig
        {
    	private:
    		GameConfig() {};
    		GameConfig(const GameConfig& tmpobj);
    		GameConfig& operator=(const GameConfig& tmpobj);
    		~GameConfig() {}
        private:
            static GameConfig* m_instance; 
            static Garbo garboobj; // 用于自动释放的嵌套类对象
    
        public:
            static GameConfig* getInstance()
            {
                // 无需判断空,直接返回已初始化的实例
                return m_instance; 
            }
        };
        // 程序启动时就初始化实例,即使构造函数私有也允许
        GameConfig* GameConfig::m_instance = new GameConfig(); 
    }
    

3.饿汉式变体(_nmsp4 版本)

将静态成员从指针改为GameConfig对象,直接返回对象地址,本质仍为饿汉式,代码如下:

namespace _nmsp4
{
    class GameConfig
    {
	private:
		GameConfig() {};
		GameConfig(const GameConfig& tmpobj);
		GameConfig& operator=(const GameConfig& tmpobj);
		~GameConfig() {}
    private:
        // 静态成员为对象而非指针
        static GameConfig m_instance; 

    public:
        static GameConfig* getInstance()
        {
            return &m_instance; // 返回对象地址
        }
    };
    // 程序启动时初始化对象
    GameConfig GameConfig::m_instance; 
}

(四)单件类对象内存释放问题

  1. 内存泄漏隐患

    基础版本中,getInstance()new的实例默认由操作系统在程序结束时回收,但调试模式下会检测到内存泄漏;部分场景需开发者主动 / 自动释放实例。

  2. 释放方案迭代

    方案 代码实现 优缺点
    手动释放 增加freeInstance()静态方法,手动调用delete m_instance 优点:逻辑简单;缺点:需手动调用,易遗漏且多线程下有安全风险
    嵌套垃圾回收类(Garbo,最优方案) GameConfig内嵌套Garbo类,利用静态对象析构机制自动释放 优点:无需手动调用,程序结束时自动释放,无内存泄漏;缺点:需增加嵌套类代码
  3. 嵌套类自动释放核心代码

    • 懒汉式中的实现(_nmsp1 版本):在getInstance()内定义静态Garbo对象,程序结束时Garbo析构触发释放

      namespace _nmsp1
      {
          class GameConfig  
          {
          private:
              // 嵌套垃圾回收类
              class Garbo
              {
              public:
                  ~Garbo()
                  {
                      if (GameConfig::m_instance != nullptr)
                      {
                          delete GameConfig::m_instance;
                          GameConfig::m_instance = nullptr;
                      }
                  }
              };
              
          /*
      	public:
      		static void freeInstance()
      		{
      			if (m_instance != nullptr)
      			{
      				delete GameConfig::m_instance;
      				GameConfig::m_instance = nullptr;
      			}
      		}
      	*/
              
          public:
              static GameConfig* getInstance()
              {
                  if (m_instance == nullptr)
                  {
                      static Garbo garboobj; // 静态对象,程序结束时析构
                      m_instance = new GameConfig();
                  }
                  return m_instance;
              }
          };
      }
      
    • 饿汉式中的实现(_nmsp3 版本):类内定义静态Garbo成员,类外初始化,析构时释放实例

      namespace _nmsp3
      {
          class GameConfig
          {
          private:
              class Garbo
              {
              public:
                  ~Garbo()
                  {
                      if (GameConfig::m_instance != nullptr)
                      {
                          delete GameConfig::m_instance;
                          GameConfig::m_instance = nullptr;
                      }
                  }
              };
              static Garbo garboobj; // 类内静态Garbo对象
              
          };
          // 类外初始化Garbo对象
          GameConfig::Garbo GameConfig::garboobj;
      }
      

(五)单件类定义、UML 图及另外一种实现方法

  1. 单件模式官方定义

    保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法(即getInstance静态成员函数)。

  2. UML 图说明

    • 类中带下划线的成员(如m_instance)表示静态成员
    • 空心菱形箭头指向自身:表示m_instance是指向本类对象的指针
    • 类的构造、拷贝构造、赋值重载、析构方法为私有,仅暴露getInstance全局访问接口
  3. 另外一种实现(_nmsp5 版本:返回局部静态对象引用)

    • 核心逻辑getInstance()返回局部静态GameConfig对象的引用,C++11 标准保证局部静态变量的初始化是线程安全的

    • 核心代码

      namespace _nmsp5
      {	
      	class GameConfig
      	{
      		//......
      	private:
      		GameConfig() {};
      		GameConfig(const GameConfig& tmpobj);
      		GameConfig& operator=(const GameConfig& tmpobj);
      		~GameConfig() {}
      	public:
      		static GameConfig& getInstance()
      		{
      			static GameConfig instance; //注意区别   函数第一次执行时被初始化的静态变量             与 
      			                            //           通过编译期常量进行初始化的基本类型静态变量
      			return instance;
      		}	
      	};	
      }
      
    • 使用方式

      _nmsp5::GameConfig& g_gc40 = _nmsp5::GameConfig::getInstance();
      
    • 关键概念区分

      • 函数内类类型局部静态变量(如getInstance()内的instance):第一次调用函数时才初始化

      • 编译期常量初始化的基本类型静态变量(如myfunc()内的stcs):程序启动时即初始化,无需调用函数,示例如下:

        int myfunc()
        {
            static int stcs = 100; // 程序启动时已赋值为100,无需调用myfunc
            stcs += 180;
            return stcs;
        }
        
  4. 多单件类相互引用的坑

    • 问题:多个单件类(如GameConfig和日志类Log)的析构顺序不确定,若在GameConfig析构函数中引用已析构的Log实例,会导致程序崩溃
    • 解决原则不要在单件类的析构函数中引用其他单件类对象

三、整体代码迭代路径

  1. v1 基础懒汉式(_nmsp1 雏形):实现单实例,但无线程安全和内存释放
  2. v2 线程安全优化:先粗暴加锁→再双重锁定→最终原子操作版本(_nmsp2),解决多线程问题
  3. v3 饿汉式实现(_nmsp3):静态成员直接初始化实例,天然线程安全
  4. v4 内存自动释放:引入嵌套Garbo类,实现实例自动释放,消除内存泄漏
  5. v5 饿汉式变体(_nmsp4):静态成员为对象而非指针,简化代码
  6. v6 局部静态引用版(_nmsp5):利用 C++11 特性简化实现,兼顾线程安全和代码简洁

四、关键注意事项

  1. 饿汉式需注意多cpp文件的全局变量初始化顺序问题,避免在全局变量中依赖单件实例
  2. 懒汉式建议在main函数创建线程前提前调用getInstance(),简化线程安全处理
  3. 多单件类交互时,严格遵守 “析构函数不引用其他单件” 的原则

参考资料来源:《C++新经典---设计模式》

posted @ 2025-12-13 16:22  CodeMagicianT  阅读(5)  评论(0)    收藏  举报