饿汉式单例在程序启动时即初始化,线程安全但可能浪费资源;懒汉式延迟初始化节省资源,但需用双重检查锁定或std::call_once保障线程安全,且析构和内存泄漏风险更高。

饿汉式单例:启动时就构造,线程安全但可能浪费资源
饿汉式在程序加载时就完成实例初始化,天然线程安全,不用加锁。但它的问题是:哪怕你整个程序一次都没调用 getInstance(),构造函数也会执行,如果构造开销大(比如读配置、连数据库),就白耗资源。
实操建议:
- 把静态成员变量直接定义在类外,或用静态局部变量(C++11 起保证首次调用时初始化且线程安全)
- 构造函数设为
private,禁止外部 new;拷贝/移动构造和赋值全删掉(= delete) - 返回引用比返回指针更常见,避免裸指针生命周期困惑
示例关键片段:
class Singleton {
private:
Singleton() { /* heavy init */ }
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static Singleton& getInstance() {
static Singleton instance; // C++11 线程安全的局部静态
return instance;
}
};懒汉式单例:第一次调用才创建,需手动处理线程安全
懒汉式延迟初始化,节省资源,但多线程环境下极易出错——两个线程同时判断 instance == nullptr 为真,接着都执行 new,结果构造两次,返回不同对象。
立即学习“C++免费学习笔记(深入)”;
常见错误现象:
- 没加锁,多线程下出现多个实例(
getInstance()返回地址不一致) - 只对 new 加锁,但没对判空加锁,仍可能重复构造
- 用
std::call_once+std::once_flag是更简洁安全的选择(C++11)
推荐写法(双重检查锁定 DCLP,注意必须用 volatile 或原子指针防重排序,C++11 后推荐用 std::atomic):
class Singleton {
private:
static std::atomic instance;
Singleton() = default;
public:
static Singleton* getInstance() {
Singleton* ptr = instance.load();
if (ptr == nullptr) {
std::lock_guard lock(mutex_);
ptr = instance.load();
if (ptr == nullptr) {
ptr = new Singleton();
instance.store(ptr);
}
}
return ptr;
}
}; 为什么 C++11 后推荐用局部静态变量写饿汉式
因为标准明确要求:函数内静态局部变量的初始化是线程安全的,且仅发生一次。这比手写锁、std::call_once 更简洁,也比全局静态对象更可控(避免静态初始化顺序问题)。
使用场景:
- 初始化不依赖其他全局对象(否则可能因初始化顺序未定义而崩溃)
- 构造函数不抛异常(否则局部静态初始化失败后再次调用会直接 terminate)
- 不需要在程序退出时显式析构(静态局部变量析构时机由实现决定,不可控)
参数差异不大,但要注意:局部静态变量的析构函数调用顺序与构造相反,且不保证跨编译单元的顺序——这点常被忽略。
懒汉式析构和内存泄漏风险怎么处理
懒汉式用 new 分配的实例,如果不手动释放,程序退出时不会自动调用析构函数,资源(如文件句柄、内存池)可能没清理。但手动 delete 又引入“何时 delete”的新问题。
实操建议:
- 避免在懒汉式中管理需要显式清理的资源;优先用 RAII 封装(如智能指针、文件流)
- 若必须控制析构时机,可用
std::atexit()注册清理函数,但要确保该函数只执行一次,且不能依赖其他静态对象 - 更稳妥的做法是:改用饿汉式 + 局部静态变量,靠语言机制保证析构(虽然时机不可控,但至少会发生)
容易踩的坑:用 std::shared_ptr 管理懒汉式实例,却忘了它默认的删除器不调用析构函数——得传自定义删除器,否则照样泄漏。
真正麻烦的不是写法本身,而是单例生命周期和资源清理边界的模糊性。很多人卡在“要不要删”“什么时候删”“删了别人还在用怎么办”,而不是语法细节。










