单例模式:保证在整个程序中,某个类只有一个实例,并且可以全局可访问。
换句话说:
整个类只想要一个对象
全局都可以使用这个对象
不允许创建第二个对象
再比如:
程序的配置管理器(只需要一个)
日志系统(写日志的对象只有一个)
数据库连接池(共享同一个)
想用全局变量(虽然用单例模式也不太好)
懒汉单例(c++11后)
#include <iostream>
#include <mutex>
class Singleton {
public:
// 获取唯一实例的全局访问点
static Singleton& getInstance() {
static Singleton instance; // C++11保证线程安全
return instance;
}
void doSomething() {
std::cout << "我是唯一的实例,正在工作..." << std::endl;
}
private:
// 构造函数私有,禁止外部 new
Singleton() {
std::cout << "单例对象创建成功!" << std::endl;
}
// 禁止拷贝和赋值
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
int main() {
// 获取唯一实例
Singleton& s1 = Singleton::getInstance();
Singleton& s2 = Singleton::getInstance();
s1.doSomething();
// 检查是否是同一个对象
if (&s1 == &s2) {
std::cout << "s1 和 s2 是同一个实例" << std::endl;
}
return 0;
}
结果:
单例对象创建成功!
我是唯一的实例,正在工作...
s1 和 s2 是同一个实例
懒汉单例 vs 饿汉单例
在 C++ 中,常见的单例实现分为两种:
特性 | 懒汉单例(Lazy Singleton) | 饿汉单例(Eager Singleton / 普通单例) |
---|---|---|
创建时机 | 第一次使用 getInstance() 时才创建 |
程序启动时就创建 |
资源使用 | 节省资源,只有需要时才创建 | 程序一启动就分配资源,可能会浪费 |
线程安全 | C++11 以后用 static 自动线程安全 |
程序启动前初始化,天然线程安全 |
实现难度 | 稍微复杂,需要考虑懒加载 | 非常简单,直接静态初始化 |
性能 | 首次访问时稍慢,但之后一样快 | 程序启动时有初始化开销,访问快 |
适用场景 | 创建代价大、偶尔使用的对象 | 创建代价小、经常使用的对象 |
懒汉单例的实现:
class LazySingleton {
public:
static LazySingleton& getInstance() {
static LazySingleton instance; // 第一次调用时创建
return instance;
}
private:
LazySingleton() {}
LazySingleton(const LazySingleton&) = delete;
LazySingleton& operator=(const LazySingleton&) = delete;
};
只有第一次调用getInstance()才会创造对象
如果程序没有用到这个对象,就不会浪费资源
适合“可能用,也可能不用”的场景
饿汉单例的实现:
class EagerSingleton {
public:
static EagerSingleton& getInstance() {
return instance;
}
private:
EagerSingleton() {}
static EagerSingleton instance; // 程序启动时就创建
};
EagerSingleton EagerSingleton::instance; // 定义并初始化
程序一启动就会创造对象
初始化顺序再程序启动阶段完成
适合“肯定会用”的场景,比如日志系统,配置管理器
static EagerSingleton instance; 写在类外,会在程序启动时就创建
tips:
能用单例不用全局变量,能不用单例就不用单例
单例模式不仅仅是“全局变量”,它还带来更好的封装性、控制力和安全性。
特性 | 全局变量 | 单例模式 |
---|---|---|
内存控制 | 程序一启动就分配 | 可以懒加载,按需创建 |
封装性 | 所有人可以直接改值 | 对象私有,接口可控 |
可维护性 | 容易被误用 | 提供统一访问入口 |
多实例风险 | 程序员可以随便 new 多个 | 禁止拷贝、赋值,确保唯一 |
线程安全 | 需要自己管理 | C++11 的懒汉式默认安全 |
可扩展性 | 改逻辑要修改全局代码 | 可以封装到类里,扩展方便 |
那为什么能不用单例就不用单例
单例缺点:
1.单例的本质就是全局变量,说单例不好就是说全局变量不好。
全局状态会带来问题:
难以追踪谁修改了它
如果多人同时修改,可能出现不可预期的行为
线程安全变复杂
2.难以进行单元测试
举个例子,有一个 Database
单例:
Database& db = Database::getInstance();
db.connect("mysql://...");
问题来了:
如果我要写一个测试用例,让
Database
不连接真实的 MySQL,而是用一个假数据库(Mock Database),怎么办?单例让“依赖注入”变得困难,因为你无法轻易替换内部对象。
如果是普通类,我们可以这样:
class MyService {
public:
MyService(Database* db) : db_(db) {} // 构造函数,传入数据库对象指针
void work() { db_->query("SELECT 1"); } // 调用数据库执行查询
private:
Database* db_; // 保存数据库对象指针
};
测试时:
MockDatabase mockDb;
MyService service(&mockDb); // 传入假的db
如果用单例,就很难做到这一点 → 这就是很多书批评单例的原因。
3.生命周期难管理
单例通常使用静态变量,比如:
static Logger& getInstance() {
static Logger instance;
return instance;
}
这个对象会在程序退出时自动销毁,但如果在其他全局对象的析构函数中访问 Logger
,可能会出现“对象已被销毁”的错误。
静态对象的销毁顺序
在 C++ 中,静态对象(static 对象)的生命周期是:
程序运行到第一次使用它时创建(懒汉)或程序启动就创建(饿汉)
程序退出时会自动销毁(调用析构函数)
例如:
class Logger {
public:
Logger() { std::cout << "Logger创建\n"; }
~Logger() { std::cout << "Logger销毁\n"; }
void log(const std::string& msg) { std::cout << msg << std::endl; }
};
int main() {
static Logger logger;
logger.log("Hello");
return 0;
}
logger
对象会在main()
结束后自动销毁析构函数
~Logger()
会被调用
问题出现的场景
假设你还有另一个全局对象:
class GlobalObject {
public:
~GlobalObject() {
Logger::getInstance().log("GlobalObject析构");
}
};
GlobalObject g_obj; // 全局对象
程序运行顺序:
main()
结束,静态对象开始销毁全局对象
g_obj
的析构函数先执行析构函数里调用
Logger::getInstance().log()
问题来了:
如果
Logger
是静态对象(比如懒汉单例的static Logger instance;
)它的析构函数可能已经被调用(对象已销毁)
这时再调用
log()
→ 访问已销毁的对象 → 未定义行为 → 程序可能崩溃
在大型C++项目中,这个问题被称为 Static Initialization Order Fiasco(静态初始化顺序灾难)。
如果用普通类+依赖注入,可以更好地控制对象的生命周期。
4.隐式耦合,降低可维护性
单例是一种全局可见的状态,导致“想改一处,动全局”:
你在某个地方修改了单例的状态
另一个模块突然行为异常
你完全不知道问题出在哪
如果改用依赖注入(把需要的对象通过构造函数传递),模块之间的依赖会更明确。
原因 | 解释 |
---|---|
全局状态 | 单例本质是“全局变量”,会导致数据混乱 |
难测试 | 单元测试很难替换单例对象 |
生命周期不可控 | 静态对象销毁顺序不受控制 |
隐式耦合 | 模块间依赖隐藏在单例中,难以维护 |
多线程风险 | 如果用C++98或旧代码,线程安全很难保证 |
不适合用单例
数据缓存
如果缓存只是某个功能模块使用,用类成员更好临时状态管理
比如某个页面的UI状态,不需要全局唯一可扩展性要求高
如果未来有一天可能需要多个实例,提前用单例会很难改
替代方案
如果能不用单例,常见的替代方案有:
(1) 依赖注入(Dependency Injection)
不要在类内部自己创建或固定依赖,把依赖从外部传进来。
拿刚刚的数据库举例子来说,"mysql://..."
写在代码里就是 硬编码,你要测试改数据库就必须改那段源码。
class Logger {
public:
void log(const std::string& msg); //用来打印或记录日志
};
class Service {
public:
Service(Logger* logger) : logger_(logger) {} // 把传进来的 logger 参数 赋值给类成员变量 logger_
void run() { logger_->log("running"); } // 成员函数
private:
Logger* logger_; //成员变量
};
int main() {
Logger logger; //创建一个Logger对象
Service service(&logger); // 明确注入依赖,把logger的地址传进去,service就可以使用这个logger
service.run(); // Service调用Logger的log()方法
}
Service 类解析
成员变量
Logger* logger_
指向一个
Logger
对象的指针用来在
Service
内部调用Logger
的功能
构造函数
Service(Logger* logger)
接收一个
Logger
对象指针作为参数初始化成员变量
logger_
核心思想:Service 不负责创建 Logger,而是使用外部提供的 Logger → 这就是“依赖注入”
把传进来的: logger_(logger)
→ 初始化列表logger
参数 赋值给类成员变量logger_
Service
类 依赖 一个Logger
对象来记录日志Service
自己不创建Logger
,而是通过构造函数 外部传入 一个Logger
对象main()
函数创建Logger
对象,再传给Service
使用
好处:
测试更容易(可以传入Mock对象(用于测试的假对象))
生命周期由你控制
模块之间的依赖明确
(2) 把对象放到 main() 里
有些时候,其实你只需要在 main()
里创建一个对象,然后把它传递给需要的地方,完全不需要全局变量或单例。