第七章 单件模式
第七章 单件模式
一、单件模式基础认知
- 模式别称:单例模式、单态模式
- 模式类型:创建型设计模式
- 核心目标:保证一个类在整个程序中仅有一个对象实例,同时提供对该实例的全局访问方法
- 设计原则:遵循 Scott Meyers 的理念 ——要使接口 / 类型易于正确使用,难以错误使用,将 “保证单实例” 的责任交给类设计者,而非类使用者(避免使用者随意创建多实例引发逻辑混乱 / 性能问题)
- 核心实现手段:通过私有构造函数限制类外实例化,配合静态成员和静态方法实现单实例的全局访问
二、核心知识点拆解(结合代码迭代)
(一)单件类的基本概念和实现
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指向同一个实例,实现单例
(二)单件类在多线程中可能导致的问题及解决
-
多线程安全隐患
当多个线程同时调用
getInstance()时,若线程 1 执行完if (m_instance == nullptr)后被系统切走,线程 2 会进入分支创建实例;线程 1 恢复执行后会再次创建实例,导致多实例,违背单例原则。 -
解决方案迭代
方案 代码实现 优缺点 粗暴加锁 在 getInstance()开头加std::lock_guard<std::mutex> gcguard(my_mutex);优点:解决多线程问题;缺点:每次调用都加锁,频繁调用时效率极低,且对象创建后加锁无意义 双重锁定(双重检查) 在 if (m_instance == nullptr)内加锁,且加锁后再次判断空优点:仅对象未创建时加锁,提升效率;缺点:存在内存访问重排序问题(编译器指令重排,可能返回未初始化的实例指针) C++11 原子操作 + 内存栅栏(_nmsp2 版本) 用 std::atomic修饰m_instance,配合内存栅栏禁止指令重排优点:彻底解决双重锁定的重排序问题;缺点:代码复杂度高,需引入 <atomic>头文件最优简易方案 在 main函数创建任何线程前,先调用一次GameConfig::getInstance()优点:提前初始化实例,后续多线程调用无需加锁,简单高效且无性能损耗 -
原子操作版本核心代码(_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初始化为nullptr,getInstance()内判断空后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;
}
(四)单件类对象内存释放问题
-
内存泄漏隐患
基础版本中,
getInstance()内new的实例默认由操作系统在程序结束时回收,但调试模式下会检测到内存泄漏;部分场景需开发者主动 / 自动释放实例。 -
释放方案迭代
方案 代码实现 优缺点 手动释放 增加 freeInstance()静态方法,手动调用delete m_instance优点:逻辑简单;缺点:需手动调用,易遗漏且多线程下有安全风险 嵌套垃圾回收类(Garbo,最优方案) 在 GameConfig内嵌套Garbo类,利用静态对象析构机制自动释放优点:无需手动调用,程序结束时自动释放,无内存泄漏;缺点:需增加嵌套类代码 -
嵌套类自动释放核心代码
-
懒汉式中的实现(_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 图及另外一种实现方法
-
单件模式官方定义
保证一个类仅有一个实例存在,同时提供能对该实例访问的全局方法(即
getInstance静态成员函数)。 -
UML 图说明
- 类中带下划线的成员(如
m_instance)表示静态成员 - 空心菱形箭头指向自身:表示
m_instance是指向本类对象的指针 - 类的构造、拷贝构造、赋值重载、析构方法为私有,仅暴露
getInstance全局访问接口
- 类中带下划线的成员(如
-
另外一种实现(_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; }
-
-
-
多单件类相互引用的坑
- 问题:多个单件类(如
GameConfig和日志类Log)的析构顺序不确定,若在GameConfig析构函数中引用已析构的Log实例,会导致程序崩溃 - 解决原则:不要在单件类的析构函数中引用其他单件类对象
- 问题:多个单件类(如
三、整体代码迭代路径
- v1 基础懒汉式(_nmsp1 雏形):实现单实例,但无线程安全和内存释放
- v2 线程安全优化:先粗暴加锁→再双重锁定→最终原子操作版本(_nmsp2),解决多线程问题
- v3 饿汉式实现(_nmsp3):静态成员直接初始化实例,天然线程安全
- v4 内存自动释放:引入嵌套
Garbo类,实现实例自动释放,消除内存泄漏 - v5 饿汉式变体(_nmsp4):静态成员为对象而非指针,简化代码
- v6 局部静态引用版(_nmsp5):利用 C++11 特性简化实现,兼顾线程安全和代码简洁
四、关键注意事项
- 饿汉式需注意多
cpp文件的全局变量初始化顺序问题,避免在全局变量中依赖单件实例 - 懒汉式建议在
main函数创建线程前提前调用getInstance(),简化线程安全处理 - 多单件类交互时,严格遵守 “析构函数不引用其他单件” 的原则
参考资料来源:《C++新经典---设计模式》

浙公网安备 33010602011771号