在现代 C++ 开发中,单例模式经常被用来保证某个类只有一个实例,并且在整个程序生命周期内保持可访问。传统实现往往使用双重检查锁(Double-Checked Locking, DCL),但其实现细节与编译器优化紧密相关,容易出现不可预知的错误。幸运的是,自 C++11 起,标准库提供了对线程安全的静态局部变量初始化的保证,使得实现线程安全的单例变得异常简单且高效。
1. 基于静态局部变量的单例实现
#include <iostream>
#include <mutex>
class Logger {
public:
// 提供全局访问点
static Logger& instance() {
// C++11 保证局部静态变量在第一次访问时是线程安全的
static Logger instance;
return instance;
}
// 示例方法:记录消息
void log(const std::string& msg) {
std::lock_guard<std::mutex> lock(mtx_);
std::cout << "[LOG] " << msg << std::endl;
}
// 禁止拷贝和移动
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
private:
// 私有构造函数,防止外部实例化
Logger() = default;
~Logger() = default;
std::mutex mtx_; // 用于保护日志输出
};
关键点说明
-
局部静态变量
static Logger instance;在第一次调用instance()时才会被构造,并且编译器保证在多线程环境下只会执行一次构造过程。 -
禁止拷贝和移动
删除拷贝构造和移动构造函数,确保不会意外创建多个实例。 -
线程安全的操作
虽然单例的构造已线程安全,但实例内部的状态修改(如日志写入)仍需要同步。这里使用std::mutex结合std::lock_guard实现。
2. 与 DCL 的比较
传统的双重检查锁实现代码如下:
class Logger {
public:
static Logger* instance() {
if (!instance_) { // 第一次检查
std::lock_guard<std::mutex> lock(mtx_);
if (!instance_) { // 第二次检查
instance_ = new Logger();
}
}
return instance_;
}
private:
static Logger* instance_;
static std::mutex mtx_;
// ...
};
这种方式需要手动管理实例的销毁(delete),并且如果编译器没有严格遵循内存模型,可能出现构造未完成就返回指针的情况。相比之下,C++11 的局部静态变量实现更简洁、安全,并且不需要手动 delete。
3. 延迟销毁与程序退出
由于单例是局部静态变量,程序结束时会自动调用其析构函数。若单例持有非线程安全资源(例如文件句柄),需在析构中做相应清理。
4. 高级用法:懒加载与重构
如果单例需要延迟加载且可能在多线程环境下被多次初始化,仍可以利用 std::call_once 与 std::once_flag:
class Config {
public:
static Config& instance() {
std::call_once(flag_, [](){
instance_ = new Config();
});
return *instance_;
}
private:
static Config* instance_;
static std::once_flag flag_;
// ...
};
但对于大多数情况,局部静态变量已足够。
5. 小结
- C++11 提供了对局部静态变量初始化的线程安全保证,简化单例实现。
- 避免 使用传统 DCL,减少潜在错误。
- 注意 单例内部状态修改仍需同步。
- 销毁 由编译器自动处理,避免内存泄漏。
通过上述方法,你可以在任何需要全局唯一实例的场景中,安全、简洁地实现单例模式。