在现代 C++(C++11 及以后)中实现线程安全的单例模式比以往任何时候都简单、可靠。下面从设计思路、实现细节、性能考虑以及常见误区四个方面进行阐述,并给出完整可编译的示例代码。
一、设计思路
-
懒初始化
单例对象只在第一次使用时创建,避免不必要的资源占用。 -
线程安全
需要保证在多线程环境下只创建一个实例,并且在实例创建后对其的访问也是安全的。 -
全局访问
通过Singleton::instance()或类似的静态成员函数提供全局入口。 -
防止拷贝/移动
禁用拷贝构造、移动构造和赋值操作,保证只有一个对象存在。
二、实现细节
2.1 传统 Meyers 单例(C++11 以后)
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // C++11 保证线程安全
return instance;
}
// 业务接口
void doSomething() { /* ... */ }
private:
Singleton() = default;
~Singleton() = default;
// 禁止拷贝、移动
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
};
优点
- 代码简洁,几行即可完成
- C++11 标准保证了
static局部变量的初始化线程安全
缺点
- 若单例需要在程序退出前执行自定义清理,可能会出现析构顺序问题(尤其在多翻译单元情况下)
2.2 经典双重检查锁(不推荐)
class Singleton {
public:
static Singleton* instance() {
if (!ptr_) { // 第一重检查
std::lock_guard<std::mutex> lock(mutex_);
if (!ptr_) { // 第二重检查
ptr_ = new Singleton();
}
}
return ptr_;
}
private:
static Singleton* ptr_;
static std::mutex mutex_;
Singleton() = default;
};
Singleton* Singleton::ptr_ = nullptr;
std::mutex Singleton::mutex_;
不推荐
- 复杂度高,易出错
- 需要手动管理内存和析构
2.3 智能指针 + std::call_once(兼顾延迟加载与析构)
class Singleton {
public:
static Singleton& instance() {
std::call_once(flag_, []() {
ptr_.reset(new Singleton());
});
return *ptr_;
}
private:
Singleton() = default;
~Singleton() = default;
static std::unique_ptr <Singleton> ptr_;
static std::once_flag flag_;
};
std::unique_ptr <Singleton> Singleton::ptr_;
std::once_flag Singleton::flag_;
std::once_flag与std::call_once结合使用,确保初始化只执行一次unique_ptr自动释放资源,避免内存泄漏- 适用于需要在销毁时做清理或不想使用局部静态对象的场景
三、性能与可测性
- 局部静态 的实现几乎不占用额外内存,且初始化成本仅一次。
std::call_once也只会在第一次调用时花费少量锁开销,后续调用几乎无锁。- 如果单例是高成本对象,建议使用 懒加载 并在必要时显式销毁,或者使用
std::shared_ptr并将其生命周期与业务对象绑定。
四、常见误区
| 误区 | 正确做法 |
|---|---|
认为 new 后的单例不需要 delete |
使用 std::unique_ptr 或局部静态即可自动销毁 |
直接使用宏 #define SINGLETON |
宏无法提供类型安全,易导致命名冲突 |
| 忽略拷贝/移动构造 | 必须显式 delete 相关函数,防止生成多实例 |
| 在多文件项目中把单例定义在头文件 | 需要 inline 或 constexpr 静态成员,或者使用单独的源文件实现 |
五、完整示例(Meyers 单例)
#include <iostream>
#include <string>
class Logger {
public:
static Logger& get() {
static Logger instance; // C++11 线程安全
return instance;
}
void log(const std::string& msg) {
// 简单示例:输出到标准输出
std::cout << "[LOG] " << msg << '\n';
}
private:
Logger() = default;
~Logger() = default;
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
Logger(Logger&&) = delete;
Logger& operator=(Logger&&) = delete;
};
int main() {
Logger::get().log("程序开始");
Logger::get().log("这是第二条日志");
return 0;
}
Logger是单例日志类,任何线程均可安全调用log()。- 由于局部静态的实现,日志实例在第一次
get()时创建,程序退出时自动销毁。 - 禁用拷贝/移动确保了单例唯一性。
六、总结
- 在 C++11 之后,Meyers 单例(局部静态)已足以满足大多数需求,代码简洁且线程安全。
- 若需要自定义销毁顺序或更细粒度的控制,可结合
std::call_once+std::unique_ptr。 - 始终禁用拷贝/移动,避免多实例。
- 关注单例的生命周期与资源管理,防止内存泄漏或析构顺序问题。
掌握上述模式后,你就能在 C++ 项目中安全、灵活地使用单例。祝编码愉快!