目录
2.4 局部静态变量单例:“C++11 的魔法,一行代码搞定线程安全”

class 卑微码农:
def __init__(self):
self.技能 = ['能读懂十年前祖传代码', '擅长用Ctrl+C/V搭建世界', '信奉"能跑就别动"的玄学']
self.发量 = 100 # 初始发量
self.咖啡因耐受度 = '极限'
def 修Bug(self, bug):
try:
# 试图用玄学解决问题
if bug.严重程度 == '离谱':
print("这一定是环境问题!")
else:
print("让我看看是谁又没写注释...哦,是我自己。")
except Exception as e:
# 如果try块都救不了,那就...
print("重启一下试试?")
self.发量 -= 1 # 每解决一个bug,头发-1
# 实例化一个我
我 = 卑微码农()
引言:为什么你的全局对象总出问题?

作为 C++ 开发者,你一定写过这样的代码:
// 全局配置对象,到处用
Config g_config;
// 日志工具,全局调用
Logger g_logger;
int main() {
g_config.load("config.ini"); // 初始化配置
g_logger.init(g_config.getLogPath()); // 用配置初始化日志
// 其他模块直接用g_config和g_logger
UserModule user;
user.setConfig(&g_config);
user.setLogger(&g_logger);
// ...
}
这段代码看似简单,却藏着 3 个致命问题:
- 初始化顺序混乱:如果
g_logger.init用到g_config,但g_config还没加载(全局对象初始化顺序不确定),直接崩溃; - 多线程灾难:如果多个线程同时操作
g_config(比如一个读配置,一个更新配置),没有锁保护,数据会错乱; - 重复创建:如果其他文件也定义了
Config实例,或者不小心new Config,就会出现多个配置对象,导致数据不一致。
这些问题的根源,在于全局对象缺乏 “唯一性” 和 “可控性”—— 你没法保证它只被创建一次,也没法控制它的初始化时机和访问顺序。
而单例模式(Singleton Pattern),就是专门解决 “全局唯一对象” 问题的设计模式:它能确保一个类只有一个实例,提供全局访问点,并且由类自身控制实例的创建和生命周期。
本文会用 C++ 实战场景 + 可运行代码 + 真实踩坑经验,带你彻底吃透单例模式。从最简单的 “饿汉式” 到线程安全的 “双重检查锁定”,从常见错误到工业级实现,读完这篇,你再也不会为 “全局唯一对象” 头疼。
一、先搞懂:单例模式到底是什么?

1.1 核心思想:“一个类,只有一个对象,全局能用”
单例模式的核心是:保证一个类在整个程序生命周期中只有一个实例,并提供一个全局访问点,避免实例被重复创建。
用一句大白话解释:单例就是 “全局唯一的对象”,但比普通全局变量更靠谱。比如:
- 公司的 CEO:整个公司只有一个,所有人都能找到他(全局访问),且只能由公司成立时任命(自行控制创建);
- 游戏中的玩家角色:单人游戏里只有一个主角,所有系统(任务、背包、战斗)都要和他交互,不能同时存在两个主角。
单例模式解决的不是 “全局访问”(普通全局变量也能做到),而是 **“唯一性” 和 “可控性”**—— 确保无论怎么调用,都只能得到同一个实例,并且初始化、销毁都有明确的时机。
1.2 单例模式的 3 个核心要素(缺一不可)
一个合格的单例必须满足 3 个条件:
- 唯一实例:类只能有一个实例,不能通过
new、拷贝等方式创建多个; - 自行创建:实例必须由类自身创建,而不是外部强制生成;
- 全局访问:提供一个全局访问点(通常是
getInstance()方法),让其他模块能获取这个实例。
举个反例:如果一个类虽然提供了getInstance(),但没禁用拷贝构造,那么通过Singleton s = *Singleton::getInstance()就能复制出一个新对象,这就不是单例。
1.3 什么时候该用单例?3 个典型场景
单例不是万能的,滥用会导致代码耦合严重(所有模块都依赖单例)。但以下 3 种场景,单例是最优解:
- 全局资源管理:如配置管理器(整个程序只能有一套配置)、日志器(所有日志输出到同一个地方)、连接池(数据库连接池全局唯一,避免重复创建连接);
- 状态共享:如全局计数器(记录程序运行中的事件总数,必须唯一)、用户会话(整个程序只有一个当前登录用户);
- 重量级对象:创建成本高的对象(如大型缓存、硬件设备控制器),重复创建会浪费资源,必须保证唯一。
简单说:当你需要 “一个对象被全局共享,且不能有多个实例” 时,就该考虑单例。
二、C++ 单例模式 6 种实现方式:从简单到工业级
单例模式的实现方式很多,不同场景适合不同实现。下面从最简单的 “饿汉式” 开始,逐步讲解 6 种实现,每种都附完整代码和优缺点分析。
2.1 饿汉式单例:“程序一启动就创建,管你用不用”

核心思路:
程序启动时(main 函数前)就创建单例实例,不管后续是否使用。因为初始化早,像 “饿汉” 一样提前准备好,所以叫饿汉式。
代码实现:
// SingletonHungry.h
#ifndef SINGLETON_HUNGRY_H
#define SINGLETON_HUNGRY_H
#include <iostream>
// 饿汉式单例:程序启动时创建实例
class SingletonHungry {
public:
// 禁用拷贝构造和赋值运算符(关键!避免复制出多个实例)
SingletonHungry(const SingletonHungry&) = delete;
SingletonHungry& operator=(const SingletonHungry&) = delete;
// 全局访问点
static SingletonHungry& getInstance() {
return instance; // 直接返回已创建的实例
}
// 单例的业务方法(示例)
void doSomething() {
std::cout << "饿汉式单例:执行操作,实例地址=" << this << std::endl;
}
private:
// 私有构造函数(禁止外部new)
SingletonHungry() {
std::cout << "饿汉式单例:构造函数被调用" << std::endl;
}
// 私有析构函数(可选,控制销毁逻辑)
~SingletonHungry() {
std::cout << "饿汉式单例:析构函数被调用" << std::endl;
}
// 静态成员变量:存储唯一实例
static SingletonHungry instance;
};
// 初始化静态成员(程序启动时执行,早于main函数)
SingletonHungry SingletonHungry::instance;
#endif // SINGLETON_HUNGRY_H
测试代码:
// main.cpp
#include "SingletonHungry.h"
#include <thread>
#include <chrono>
void testHungry() {
// 多个线程获取实例,验证是否唯一
for (int i = 0; i < 3; ++i) {
std::thread t([](){
SingletonHungry::getInstance().doSomething();
});
t.join();
}
}
int main() {
std::cout << "main函数开始执行" << std::endl;
testHungry();
return 0;
}
运行结果:
饿汉式单例:构造函数被调用 // 注意:这行在main函数前输出!
main函数开始执行
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:执行操作,实例地址=0x55f8a5c6c120
饿汉式单例:析构函数被调用 // 程序退出时执行
优点:
- 实现简单:几行代码就能搞定,无需复杂的线程同步;
- 线程安全:全局静态变量在程序启动时初始化,此时没有多线程问题,天然线程安全;
- 访问高效:
getInstance()直接返回实例,无需判断和锁,性能开销小。
缺点:
- 初始化过早:程序启动时就创建实例,即使整个程序都没用到,也会占用内存(比如一个很少用到的日志器,启动就占内存);
- 无法传参:构造函数不能带参数(因为静态成员初始化时无法传递参数),如果单例需要配置信息(如日志路径),就无法满足;
- 初始化顺序问题:多个饿汉式单例之间的初始化顺序不确定(全局变量初始化顺序是未定义的),如果 A 单例的构造依赖 B 单例,可能导致崩溃。
适用场景:
- 单例实例体积小,创建成本低;
- 不需要参数初始化;
- 程序启动后肯定会用到(如核心配置管理器)。
2.2 懒汉式单例(基础版):“用到的时候再创建,省内存”

核心思路:
实例在第一次调用getInstance()时才创建,而不是程序启动时。像 “懒汉” 一样,不到万不得已不干活,所以叫懒汉式。
代码实现:
// SingletonLazyBasic.h
#ifndef SINGLETON_LAZY_BASIC_H
#define SINGLETON_LAZY_BASIC_H
#include <iostream>
// 懒汉式单例(基础版,非线程安全)
class SingletonLazyBasic {
public:
// 禁用拷贝和赋值
SingletonLazyBasic(const SingletonLazyBasic&) = delete;
SingletonLazyBasic& operator=(const SingletonLazyBasic&) = delete;
// 全局访问点:第一次调用时创建实例
static SingletonLazyBasic* getInstance() {
if (instance == nullptr) { // 没创建过,就new一个
instance = new SingletonLazyBasic();
}
return instance;
}
// 业务方法
void doSomething() {
std::cout << "基础懒汉式单例:执行操作,实例地址=" << this << std::endl;
}
private:
// 私有构造
SingletonLazyBasic() {
std::cout << "基础懒汉式单例:构造函数被调用" << std::endl;
}
// 注意:基础版通常不处理析构,可能导致内存泄漏(后面会解决)
~SingletonLazyBasic() {
std::cout << "基础懒汉式单例:析构函数被调用" << std::endl;
}
// 静态指针:存储实例(初始为nullptr)
static SingletonLazyBasic* instance;
};
// 初始化静态指针为nullptr
SingletonLazyBasic* SingletonLazyBasic::instance = nullptr;
#endif // SINGLETON_LAZY_BASIC_H
测试代码(单线程):
#include "SingletonLazyBasic.h"
void testLazyBasicSingleThread() {
std::cout << "第一次获取实例:" << std::endl;
auto s1 = SingletonLazyBasic::getInstance();
s1->doSomething();
std::cout << "第二次获取实例:" << std::endl;
auto s2 = SingletonLazyBasic::getInstance();
s2->doSomething();
// 验证是否为同一实例
std::cout << "s1和s2是否相同?" << (s1 == s2 ? "是" : "否") << std::endl;
}
int main() {
std::cout << "main函数开始执行" << std::endl;
testLazyBasicSingleThread();
return 0;
}
单线程运行结果:
main函数开始执行
第一次获取实例:
基础懒汉式单例:构造函数被调用
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
第二次获取实例:
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
s1和s2是否相同?是
问题暴露(多线程测试):
void testLazyBasicMultiThread() {
// 多个线程同时调用getInstance()
std::thread t1([](){
auto s = SingletonLazyBasic::getInstance();
s->doSomething();
});
std::thread t2([](){
auto s = SingletonLazyBasic::getInstance();
s->doSomething();
});
t1.join();
t2.join();
}
int main() {
testLazyBasicMultiThread();
return 0;
}
多线程可能的运行结果(崩溃或多实例):
基础懒汉式单例:构造函数被调用 // 线程1进入,创建实例
基础懒汉式单例:构造函数被调用 // 线程2同时进入,也创建实例(问题!)
基础懒汉式单例:执行操作,实例地址=0x55d9b6f83e70
基础懒汉式单例:执行操作,实例地址=0x55d9b6f842a0 // 地址不同,说明多个实例
优点:
- 延迟初始化:用到时才创建,节省内存(适合大型对象或不常用的单例);
- 支持传参:可以在
getInstance()中传递参数给构造函数(后面优化版会实现)。
缺点:
- 线程不安全:多线程同时调用
getInstance()时,可能创建多个实例(如上面的测试); - 内存泄漏风险:
new出来的实例如果不手动delete,程序退出时不会调用析构函数(基础版没解决)。
适用场景:
- 单线程环境;
- 实例创建成本高,且不一定会用到。
2.3 懒汉式单例(线程安全版):“加锁防并发,安全第一”

核心思路:
在基础版懒汉式的基础上,给getInstance()加互斥锁(std::mutex),确保同一时间只有一个线程能执行实例创建逻辑,解决多线程安全问题。
代码实现:
// SingletonLazySafe.h
#ifndef SINGLETON_LAZY_SAFE_H
#define SINGLETON_LAZY_SAFE_H
#include <iostream>
#include <mutex> // 互斥锁
#include <memory> // 智能指针(解决内存泄漏)
// 懒汉式单例(线程安全版)
class SingletonLazySafe {
public:
// 禁用拷贝和赋值
SingletonLazySafe(const SingletonLazySafe&) = delete;
SingletonLazySafe& operator=(const SingletonLazySafe&) = delete;
// 全局访问点:加锁确保线程安全
static std::shared_ptr<SingletonLazySafe> getInstance() {
// 双重检查锁定(后面会讲为什么双重检查)
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) {
// 用智能指针管理,避免内存泄漏
instance = std::shared_ptr<SingletonLazySafe>(new SingletonLazySafe());
}
}
return instance;
}
// 业务方法
void doSomething() {
std::cout << "线程安全懒汉式单例:执行操作,实例地址=" << this << std::endl;
}
private:
// 私有构造(支持传参,这里简化)
SingletonLazySafe() {
std::cout << "线程安全懒汉式单例:构造函数被调用" << std::endl;
}
~SingletonLazySafe() {
std::cout << "线程安全懒汉式单例:析构函数被调用" << std::endl;
}
// 静态智能指针:存储实例
static std::shared_ptr<SingletonLazySafe> instance;
// 静态互斥锁:确保线程安全
static std::mutex mtx;
};
// 初始化静态成员
std::shared_ptr<SingletonLazySafe> SingletonLazySafe::instance = nullptr;
std::mutex SingletonLazySafe::mtx;
#endif // SINGLETON_LAZY_SAFE_H
测试代码(多线程):
#include "SingletonLazySafe.h"
#include <thread>
void testLazySafeMultiThread() {
// 5个线程同时获取实例
std::thread ts[5];
for (int i = 0; i < 5; ++i) {
ts[i] = std::thread([](){
auto s = SingletonLazySafe::getInstance();
s->doSomething();
});
}
for (auto& t : ts) {
t.join();
}
}
int main() {
testLazySafeMultiThread();
return 0;
}
运行结果:
线程安全懒汉式单例:构造函数被调用 // 只调用一次
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:执行操作,实例地址=0x55f1b6a5e2a0
线程安全懒汉式单例:析构函数被调用 // 智能指针自动释放
关键优化点:
- 互斥锁(
std::mutex):确保同一时间只有一个线程进入实例创建逻辑,解决多线程并发问题; - 智能指针(
std::shared_ptr):自动管理实例生命周期,程序退出时调用析构函数,避免内存泄漏; - 双重检查锁定:先判断
instance == nullptr,再加锁,加锁后再判断一次 —— 避免每次调用getInstance()都加锁(加锁是有性能开销的),提高效率。
优点:
- 线程安全:多线程环境下不会创建多个实例;
- 延迟初始化:用到时才创建,节省内存;
- 无内存泄漏:智能指针自动释放资源;
- 支持传参:可以在
getInstance()中传递参数(如下)。
支持传参的改进:
// 带参数的getInstance()
static std::shared_ptr<SingletonLazySafe> getInstance(const std::string& config) {
if (instance == nullptr) {
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) {
instance = std::shared_ptr<SingletonLazySafe>(new SingletonLazySafe(config));
}
}
return instance;
}
// 带参数的构造函数
private:
explicit SingletonLazySafe(const std::string& config) {
std::cout << "初始化单例,配置:" << config << std::endl;
}
缺点:
- 实现稍复杂:需要处理锁和智能指针;
- 性能开销:虽然双重检查减少了加锁次数,但首次调用仍有锁的开销(不过对大多数场景影响不大)。
适用场景:
- 多线程环境;
- 需要延迟初始化或带参数的单例(如日志器需要日志路径参数)。
2.4 局部静态变量单例:“C++11 的魔法,一行代码搞定线程安全”

核心思路:
利用 C++11 的特性:局部静态变量的初始化是线程安全的。在getInstance()中定义一个局部静态变量,第一次调用时初始化,后续调用直接返回,既保证唯一,又天然线程安全。
代码实现:
// SingletonLocalStatic.h
#ifndef SINGLETON_LOCAL_STATIC_H
#define SINGLETON_LOCAL_STATIC_H
#include <iostream>
// 局部静态变量单例(C++11及以上)
class SingletonLocalStatic {
public:
// 禁用拷贝和赋值
SingletonLocalStatic(const SingletonLocalStatic&) = delete;
SingletonLocalStatic& operator=(const SingletonLocalStatic&) = delete;
// 全局访问点:局部静态变量自动保证唯一和线程安全
static SingletonLocalStatic& getInstance() {
static SingletonLocalStatic instance; // 局部静态变量,第一次调用时初始化
return instance;
}
// 带参数的版本(C++11支持)
static SingletonLocalStatic& getInstance(const std::string& config) {
static SingletonLocalStatic instance(config); // 支持传参
return instance;
}
// 业务方法
void doSomething() {
std::cout << "局部静态单例:执行操作,实例地址=" << this << std::endl;
}
private:
// 无参构造
SingletonLocalStatic() {
std::cout << "局部静态单例:无参构造被调用" << std::endl;
}
// 带参构造
explicit SingletonLocalStatic(const std::string& config) {
std::cout << "局部静态单例:带参构造被调用,配置=" << config << std::endl;
}
~SingletonLocalStatic() {
std::cout << "局部静态单例:析构函数被调用" << std::endl;
}
};
#endif // SINGLETON_LOCAL_STATIC_H
测试代码(多线程 + 带参数):
#include "SingletonLocalStatic.h"
#include <thread>
void testLocalStaticMultiThread() {
// 多线程调用带参数的getInstance()
std::thread t1([](){
auto& s = SingletonLocalStatic::getInstance("log_path=/var/log");
s.doSomething();
});
std::thread t2([](){
auto& s = SingletonLocalStatic::getInstance("log_path=/var/log"); // 传相同参数
s.doSomething();
});
t1.join();
t2.join();
}
int main() {
testLocalStaticMultiThread();
return 0;
}
运行结果:
局部静态单例:带参构造被调用,配置=log_path=/var/log // 只初始化一次
局部静态单例:执行操作,实例地址=0x55e8d6f5c140
局部静态单例:执行操作,实例地址=0x55e8d6f5c140
局部静态单例:析构函数被调用 // 程序退出时自动调用
核心原理(C++11 标准):
C++11 规定,如果多个线程同时初始化同一个局部静态变量,初始化会被保证只执行一次(编译器会自动加入线程同步逻辑)。这意味着:
- 不需要手动加锁,编译器帮你搞定线程安全;
- 局部静态变量在第一次调用时初始化(延迟初始化);
- 程序退出时,局部静态变量会自动析构(无内存泄漏)。
优点:
- 实现极简:几行代码搞定,比加锁的懒汉式简单;
- 线程安全:C++11 及以上标准保证初始化安全;
- 延迟初始化:用到时才创建;
- 无内存泄漏:自动析构;
- 支持传参:可以定义带参数的
getInstance()(注意参数必须一致,否则多次调用可能出问题)。
缺点:
- 依赖 C++11 及以上标准:老编译器(如 VS2010 以前)可能不支持;
- 参数一致性问题:如果多次调用
getInstance()传不同参数,只有第一次的参数有效(后续调用会忽略参数),可能导致误解。
适用场景:
- C++11 及以上环境;
- 希望代码简洁,同时需要线程安全和延迟初始化的场景(推荐首选)。
2.5 模板单例:“一次实现,所有类都能用”

核心思路:
将单例逻辑封装成模板类,其他类只需继承该模板,就能自动成为单例。避免重复编写单例代码,提高复用性。
代码实现:
// SingletonTemplate.h
#ifndef SINGLETON_TEMPLATE_H
#define SINGLETON_TEMPLATE_H
#include <iostream>
// 模板单例基类(CRTP:奇异递归模板模式)
template <typename T>
class SingletonTemplate {
public:
// 禁用拷贝和赋值
SingletonTemplate(const SingletonTemplate&) = delete;
SingletonTemplate& operator=(const SingletonTemplate&) = delete;
// 全局访问点:返回子类的实例
static T& getInstance() {
static T instance; // 局部静态变量,线程安全(C++11)
return instance;
}
protected:
// 保护构造和析构,允许子类继承
SingletonTemplate() = default;
~SingletonTemplate() = default;
};
// ---------------------- 示例:如何使用模板单例 ----------------------
// 1. 定义需要成为单例的类,继承SingletonTemplate,并传入自身类型
class Logger : public SingletonTemplate<Logger> {
// 必须声明模板类为友元,否则模板无法访问Logger的私有构造
friend class SingletonTemplate<Logger>;
public:
void log(const std::string& msg) {
std::cout << "[日志] " << msg << std::endl;
}
private:
// 私有构造,确保只能通过模板创建
Logger() {
std::cout << "Logger单例初始化" << std::endl;
}
};
// 2. 另一个单例类:配置管理器
class ConfigManager : public SingletonTemplate<ConfigManager> {
friend class SingletonTemplate<ConfigManager>;
public:
std::string getConfig(const std::string& key) {
// 模拟获取配置
return "配置值:" + key;
}
private:
ConfigManager() {
std::cout << "ConfigManager单例初始化" << std::endl;
}
};
#endif // SINGLETON_TEMPLATE_H
测试代码:
#include "SingletonTemplate.h"
void testTemplateSingleton() {
// 使用Logger单例
Logger::getInstance().log("程序启动");
// 使用ConfigManager单例
std::cout << ConfigManager::getInstance().getConfig("log_level") << std::endl;
// 验证唯一性
Logger& log1 = Logger::getInstance();
Logger& log2 = Logger::getInstance();
std::cout << "Logger实例是否唯一?" << (&log1 == &log2 ? "是" : "否") << std::endl;
}
int main() {
testTemplateSingleton();
return 0;
}
运行结果:
Logger单例初始化
[日志] 程序启动
ConfigManager单例初始化
配置值:log_level
Logger实例是否唯一?是
核心技巧(CRTP 模式):
模板单例使用了奇异递归模板模式(CRTP):class T : public SingletonTemplate<T>,即子类继承模板类时,将自身作为模板参数传入。这样模板类就能通过T访问子类的构造函数(需声明友元)。
优点:
- 代码复用:一次实现模板,所有类都能快速变成单例,避免重复劳动;
- 自动继承特性:子类自动获得
getInstance()、线程安全、延迟初始化等特性; - 灵活性高:每个子类都是独立的单例,互不影响(Logger 和 ConfigManager 是不同的单例)。
缺点:
- 理解门槛:CRTP 模式对新手来说稍难理解;
- 构造函数限制:子类必须将模板类声明为友元,且构造函数必须私有;
- 不支持带参构造:模板的
getInstance()是固定的,如需传参,需要额外扩展(如下)。
带参模板单例扩展:
// 带参数的模板单例
template <typename T>
class SingletonTemplateWithArgs {
public:
template <typename... Args>
static T& getInstance(Args&&... args) {
static T instance(std::forward<Args>(args)...); // 完美转发参数
return instance;
}
// 禁用拷贝和赋值(同上)
// ...
};
// 使用示例
class LoggerWithArgs : public SingletonTemplateWithArgs<LoggerWithArgs> {
friend class SingletonTemplateWithArgs<LoggerWithArgs>;
public:
void log(const std::string& msg) { /* ... */ }
private:
explicit LoggerWithArgs(const std::string& path) {
std::cout << "Logger初始化,路径:" << path << std::endl;
}
};
// 调用
LoggerWithArgs::getInstance("/var/log/app.log").log("test");
适用场景:
- 项目中需要多个单例类(如日志、配置、连接池);
- 希望减少重复代码,提高开发效率。
2.6 枚举单例:“极简中的极简,一行代码实现”

核心思路:
利用 C++ 枚举的特性:枚举常量是全局唯一的。通过定义包含一个元素的枚举,实现最简单的单例(适合无状态单例)。
代码实现:
// SingletonEnum.h
#ifndef SINGLETON_ENUM_H
#define SINGLETON_ENUM_H
#include <iostream>
// 枚举单例:适合无状态工具类
enum class UtilitySingleton {
INSTANCE // 唯一实例
};
// 为枚举添加方法(C++中枚举本身不能有方法,需用命名空间包装)
namespace Utility {
// 工具类的业务方法
void doTask() {
std::cout << "枚举单例:执行工具任务" << std::endl;
}
// 获取单例(其实就是返回枚举常量)
UtilitySingleton getInstance() {
return UtilitySingleton::INSTANCE;
}
}
#endif // SINGLETON_ENUM_H
测试代码:
#include "SingletonEnum.h"
void testEnumSingleton() {
auto s1 = Utility::getInstance();
auto s2 = Utility::getInstance();
// 枚举常量比较,验证唯一
std::cout << "枚举实例是否唯一?" << (s1 == s2 ? "是" : "否") << std::endl;
Utility::doTask();
}
int main() {
testEnumSingleton();
return 0;
}
运行结果:
枚举实例是否唯一?是
枚举单例:执行工具任务
优点:
- 极致简单:几行代码搞定,几乎不可能出错;
- 天然唯一:枚举常量全局唯一,编译期保证;
- 无内存问题:枚举不涉及动态内存,没有泄漏风险。
缺点:
- 无状态限制:枚举本身不能有成员变量和方法,只能通过命名空间包装函数,适合纯工具类(无状态);
- 不支持初始化逻辑:无法在构造时执行初始化(如加载配置)。
适用场景:
- 无状态的全局工具类(如字符串工具、数学工具);
- 不需要初始化逻辑,只需保证全局唯一访问点。
三、避坑指南:单例模式的 6 个致命错误

单例模式看似简单,但实际开发中 90% 的手写单例都有隐藏问题。下面列出 6 个最常见的错误及解决方案。
错误 1:忘记禁用拷贝构造和赋值运算符
问题代码:
class BadSingleton {
public:
static BadSingleton& getInstance() {
static BadSingleton instance;
return instance;
}
private:
BadSingleton() = default;
// 忘记禁用拷贝和赋值!
// ~BadSingleton() = default;
};
// 测试:可以复制出多个实例
void testBadSingleton() {
BadSingleton& s1 = BadSingleton::getInstance();
BadSingleton s2 = s1; // 调用默认拷贝构造,创建新实例(问题!)
std::cout << "s1地址:" << &s1 << ",s2地址:" << &s2 << std::endl; // 地址不同
}
解决方案:
显式禁用拷贝构造和赋值运算符(C++11 及以上):
class GoodSingleton {
public:
GoodSingleton(const GoodSingleton&) = delete; // 禁用拷贝构造
GoodSingleton& operator=(const GoodSingleton&) = delete; // 禁用赋值
// ... 其他代码
};
错误 2:线程安全实现错误(只加一次锁)
问题代码:
class UnsafeSingleton {
public:
static UnsafeSingleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // 只加一次锁(错误!)
if (instance == nullptr) {
instance = new UnsafeSingleton();
}
return instance;
}
// ...
};
问题分析:
每次调用getInstance()都加锁,即使实例已经创建,会导致严重的性能开销(锁的获取和释放是有成本的)。
解决方案:
用双重检查锁定:先判断实例是否存在,不存在再加锁,加锁后再判断一次:
static UnsafeSingleton* getInstance() {
if (instance == nullptr) { // 第一次检查:避免每次加锁
std::lock_guard<std::mutex> lock(mtx);
if (instance == nullptr) { // 第二次检查:防止多线程并发创建
instance = new UnsafeSingleton();
}
}
return instance;
}
错误 3:内存泄漏(未处理析构)
问题代码:
class LeakSingleton {
public:
static LeakSingleton* getInstance() {
if (instance == nullptr) {
instance = new LeakSingleton(); // new出来的实例没delete
}
return instance;
}
// ...
private:
static LeakSingleton* instance;
};
问题分析:
new创建的实例如果不手动delete,程序退出时不会调用析构函数,导致内存泄漏(虽然程序退出后系统会回收内存,但析构中的清理逻辑(如关闭文件、释放连接)不会执行)。
解决方案:
- 用智能指针(
std::shared_ptr)管理实例; - 或提供手动销毁方法(不推荐,容易忘记调用);
- 或用局部静态变量(自动析构)。
错误 4:依赖单例的初始化顺序
问题代码:
// A单例的构造依赖B单例
class SingletonA {
public:
static SingletonA& getInstance() {
static SingletonA instance;
return instance;
}
private:
SingletonA() {
// B单例还没初始化,调用其方法会崩溃!
SingletonB::getInstance().doSomething();
}
};
class SingletonB {
// ... 类似实现
};
// 全局变量初始化顺序不确定,A可能先于B初始化
SingletonA a;
SingletonB b;
问题分析:
多个单例之间如果有依赖关系,且初始化顺序不确定(如饿汉式、全局变量),可能导致 “先初始化的单例调用未初始化的单例”,直接崩溃。
解决方案:
- 用懒汉式(局部静态变量),确保依赖的单例在被调用时才初始化;
- 明确初始化顺序:在
main函数中手动控制单例的初始化顺序。
错误 5:在析构函数中调用其他单例
问题代码:
class SingletonC {
public:
~SingletonC() {
// 析构时调用SingletonD,可能D已经被销毁!
SingletonD::getInstance().cleanup();
}
// ...
};
问题分析:
程序退出时,单例的析构顺序是不确定的。如果SingletonC的析构调用了SingletonD,而SingletonD已经析构,会导致未定义行为(崩溃)。
解决方案:
- 避免在单例析构函数中依赖其他单例;
- 用 “单例管理器” 统一控制析构顺序(复杂,不推荐)。
错误 6:滥用单例,导致代码耦合
问题场景:
项目中到处都是Singleton::getInstance().xxx(),所有模块都直接依赖单例,导致:
- 单元测试困难(无法 Mock 单例);
- 代码耦合严重,修改单例会影响所有依赖模块;
- 无法扩展(如需要多个实例时,只能重写大量代码)。
解决方案:
- 优先考虑 “依赖注入”:将单例作为参数传递给需要的模块,而非直接在模块中调用
getInstance(); - 限制单例数量:只对真正需要全局唯一的对象使用单例(如配置、日志),其他场景用普通类。
四、实战场景:单例模式在项目中的 3 个经典应用

单例模式不是 “炫技工具”,而是解决实际问题的方案。下面通过 3 个真实项目场景,展示单例的价值。
4.1 场景 1:全局配置管理器
需求:
程序启动时加载配置文件(config.ini),所有模块都需要读取配置(如数据库地址、日志级别),且配置只能加载一次。
单例实现:
// ConfigManager.h
#ifndef CONFIG_MANAGER_H
#define CONFIG_MANAGER_H
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
// 配置管理器:单例模式
class ConfigManager : public SingletonTemplate<ConfigManager> {
friend class SingletonTemplate<ConfigManager>;
public:
// 加载配置文件(只允许调用一次)
bool load(const std::string& filePath) {
if (isLoaded_) {
std::cout << "配置已加载,无需重复加载" << std::endl;
return true;
}
std::ifstream file(filePath);
if (!file.is_open()) {
std::cout << "配置文件打开失败:" << filePath << std::endl;
return false;
}
std::string line;
while (std::getline(file, line)) {
// 解析键值对(简化:key=value)
size_t pos = line.find('=');
if (pos == std::string::npos) continue;
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + 1);
configs_[key] = value;
}
isLoaded_ = true;
std::cout << "配置加载成功,共" << configs_.size() << "项" << std::endl;
return true;
}
// 获取配置值
std::string get(const std::string& key, const std::string& defaultValue = "") const {
auto it = configs_.find(key);
return it != configs_.end() ? it->second : defaultValue;
}
private:
ConfigManager() = default; // 私有构造
std::unordered_map<std::string, std::string> configs_; // 存储配置
bool isLoaded_ = false; // 是否已加载
};
#endif // CONFIG_MANAGER_H
使用示例:
#include "ConfigManager.h"
#include "SingletonTemplate.h"
int main() {
// 加载配置(全局只加载一次)
ConfigManager::getInstance().load("config.ini");
// 其他模块读取配置
std::string dbHost = ConfigManager::getInstance().get("db_host", "127.0.0.1");
std::string logLevel = ConfigManager::getInstance().get("log_level", "info");
std::cout << "数据库地址:" << dbHost << std::endl;
std::cout << "日志级别:" << logLevel << std::endl;
return 0;
}
为什么用单例:
- 配置全局唯一,不能加载多次(否则可能读取到不同配置);
- 所有模块都需要访问,单例提供统一访问点;
- 加载成本高(读文件、解析),单例确保只加载一次。
4.2 场景 2:日志器(Logger)
需求:
所有模块的日志都输出到同一个文件,且日志器需要先初始化(指定日志路径),之后才能使用。
单例实现:
// Logger.h
#ifndef LOGGER_H
#define LOGGER_H
#include <fstream>
#include <string>
#include <mutex>
#include <chrono>
#include <iomanip>
// 日志器:单例模式(线程安全)
class Logger : public SingletonTemplate<Logger> {
friend class SingletonTemplate<Logger>;
public:
// 初始化日志器(指定日志文件路径)
bool init(const std::string& logPath) {
if (isInited_) {
return true;
}
logFile_.open(logPath, std::ios::app);
if (!logFile_.is_open()) {
return false;
}
isInited_ = true;
return true;
}
// 输出日志(线程安全)
void log(const std::string& msg) {
if (!isInited_) {
return; // 未初始化,不输出
}
std::lock_guard<std::mutex> lock(mtx_); // 加锁确保线程安全
// 获取当前时间
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
logFile_ << "[" << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S") << "] " << msg << std::endl;
}
private:
Logger() = default;
~Logger() {
if (logFile_.is_open()) {
logFile_.close();
}
}
std::ofstream logFile_; // 日志文件
bool isInited_ = false; // 是否初始化
std::mutex mtx_; // 线程安全锁
};
#endif // LOGGER_H
使用示例:
#include "Logger.h"
#include "ConfigManager.h"
#include <thread>
void testLogger() {
// 从配置中获取日志路径,初始化日志器
std::string logPath = ConfigManager::getInstance().get("log_path", "app.log");
Logger::getInstance().init(logPath);
// 多线程输出日志
std::thread t1([](){
Logger::getInstance().log("线程1:用户登录");
});
std::thread t2([](){
Logger::getInstance().log("线程2:订单创建");
});
t1.join();
t2.join();
}
int main() {
ConfigManager::getInstance().load("config.ini");
testLogger();
return 0;
}
为什么用单例:
- 日志文件只能被一个日志器打开(多实例会导致日志错乱);
- 所有模块共享同一个日志输出点,确保日志格式统一;
- 初始化逻辑(打开文件)只执行一次,避免重复操作。
4.3 场景 3:数据库连接池
需求:
管理数据库连接,连接池全局唯一,所有模块从池中获取连接,避免频繁创建和关闭连接(成本高)。
单例实现:
// DbConnectionPool.h
#ifndef DB_CONNECTION_POOL_H
#define DB_CONNECTION_POOL_H
#include <queue>
#include <mutex>
#include <memory>
#include <string>
#include <iostream>
// 数据库连接(简化)
class DbConnection {
public:
bool connect(const std::string& host, int port, const std::string& user, const std::string& pwd) {
// 实际连接逻辑(简化)
std::cout << "连接数据库:" << host << ":" << port << std::endl;
return true;
}
void executeSql(const std::string& sql) {
std::cout << "执行SQL:" << sql << std::endl;
}
};
// 数据库连接池:单例模式
class DbConnectionPool : public SingletonTemplate<DbConnectionPool> {
friend class SingletonTemplate<DbConnectionPool>;
public:
// 初始化连接池
bool init(const std::string& host, int port, const std::string& user, const std::string& pwd, int maxSize) {
if (isInited_) return true;
// 创建maxSize个连接
for (int i = 0; i < maxSize; ++i) {
std::unique_ptr<DbConnection> conn(new DbConnection());
if (conn->connect(host, port, user, pwd)) {
pool_.push(std::move(conn));
}
}
if (pool_.empty()) {
return false;
}
maxSize_ = maxSize;
isInited_ = true;
std::cout << "连接池初始化完成,连接数:" << pool_.size() << std::endl;
return true;
}
// 获取连接(从池中取)
std::unique_ptr<DbConnection, void(*)(DbConnection*)> getConnection() {
std::lock_guard<std::mutex> lock(mtx_);
if (pool_.empty()) {
return {nullptr, [this](DbConnection* conn){}}; // 无可用连接
}
// 从队列取出连接,并用自定义删除器(归还连接到池)
auto conn = std::move(pool_.front());
pool_.pop();
return {conn.release(), [this](DbConnection* conn){
std::lock_guard<std::mutex> lock(mtx_);
pool_.push(std::unique_ptr<DbConnection>(conn));
}};
}
private:
DbConnectionPool() = default;
std::queue<std::unique_ptr<DbConnection>> pool_; // 连接池
int maxSize_ = 0; // 最大连接数
bool isInited_ = false; // 是否初始化
std::mutex mtx_; // 线程安全锁
};
#endif // DB_CONNECTION_POOL_H
使用示例:
#include "DbConnectionPool.h"
#include "ConfigManager.h"
int main() {
ConfigManager::getInstance().load("config.ini");
// 从配置获取数据库信息,初始化连接池
std::string dbHost = ConfigManager::getInstance().get("db_host");
int dbPort = std::stoi(ConfigManager::getInstance().get("db_port", "3306"));
std::string dbUser = ConfigManager::getInstance().get("db_user");
std::string dbPwd = ConfigManager::getInstance().get("db_pwd");
DbConnectionPool::getInstance().init(dbHost, dbPort, dbUser, dbPwd, 5);
// 获取连接并使用
auto conn = DbConnectionPool::getInstance().getConnection();
if (conn) {
conn->executeSql("SELECT * FROM users");
}
// conn超出作用域时,自动归还到连接池(自定义删除器生效)
return 0;
}
为什么用单例:
- 连接池全局唯一,避免多个池导致的资源浪费;
- 所有模块共享连接,确保连接被高效复用;
- 连接池的初始化和管理逻辑集中,便于维护。
五、总结:单例模式的选择指南与核心价值

5.1 6 种实现方式对比与选择建议
| 实现方式 | 线程安全(C++11) | 延迟初始化 | 支持传参 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 饿汉式 | 是 | 否 | 否 | 简单 | 单例体积小,启动后必用,无参数需求 |
| 基础懒汉式 | 否 | 是 | 是 | 简单 | 单线程环境 |
| 线程安全懒汉式 | 是 | 是 | 是 | 中等 | 多线程,需要参数,C++03 环境 |
| 局部静态变量 | 是 | 是 | 是 | 极简 | C++11 及以上,希望代码简洁(推荐首选) |
| 模板单例 | 是(依赖局部静态) | 是 | 是 | 中等 | 多个单例类,希望复用代码 |
| 枚举单例 | 是 | 否 | 否 | 极简 | 无状态工具类,不需要初始化 |
选择优先级:局部静态变量单例(C++11+) > 线程安全懒汉式(C++03) > 模板单例(多单例场景) > 饿汉式(简单无参) > 枚举单例(无状态) > 基础懒汉式(仅单线程)。
5.2 单例模式的核心价值
单例模式的本质不是 “全局访问”,而是 **“控制实例唯一性”**。它解决的是 “如何确保一个类在任何情况下都只有一个实例,并统一管理其生命周期” 的问题。
在实际开发中,单例的价值体现在:
- 资源保护:避免全局资源(如配置、日志文件)被多个实例竞争导致的错乱;
- 性能优化:减少重量级对象(如连接池、大型缓存)的重复创建成本;
- 逻辑清晰:通过
getInstance()明确标识 “全局唯一对象”,比匿名全局变量更易维护。
5.3 最后一句忠告
单例模式是 “双刃剑”:用得好能解决全局资源管理问题,用得不好会导致代码耦合、测试困难。记住:不要为了 “全局访问” 而用单例,只有当 “唯一性” 是核心需求时,才考虑单例。
如果你的项目中到处都是单例,可能需要反思:这些对象真的需要全局唯一吗?有没有更合适的设计(如依赖注入)?

被折叠的 条评论
为什么被折叠?



