C++如何实现单例模式?(保证全局唯一实例)

C++11起用static局部变量实现单例最安全,因编译器保证线程安全初始化;需禁用构造/拷贝/赋值,返回引用;异常或需控制销毁时改用std::call_once+std::unique_ptr。

为什么用 static 局部变量实现最安全

现代 C++(C++11 起)中,static 局部变量的初始化是线程安全的——编译器会自动插入必要的同步机制,无需手动加锁。这是目前最简洁、最可靠的方式,避免了双重检查锁定(DCLP)中因内存重排序导致的未定义行为。

常见错误是仍用老式“懒汉+pthread_mutex”或“静态指针+new”,既冗余又易出错;更糟的是在构造函数里调用虚函数或依赖其他单例,可能触发静态初始化顺序问题。

  • 必须把构造函数、拷贝/移动构造、赋值操作全部设为 private
  • 禁止使用 new 手动分配——用栈上静态对象,由编译器管理生命周期
  • 不要返回 Singleton*,直接返回 Singleton& 更安全(避免空指针、误删)
class Singleton {
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(Singleton&&) = delete;

public: static Singleton& getInstance() { static Singleton instance; // C++11 线程安全初始化 return instance; }

void doSomething() { /* ... */ }

};

什么时候不能用 static 局部变量?

当单例需要显式控制销毁时机(比如依赖其他全局对象析构顺序),或构造函数可能抛异常时,static 局部变量就不适用了——异常会导致初始化失败且后续调用永远抛 std::bad_function_call 或死锁。

此时应改用 std::unique_ptr + std::call_once,手动管理实例和销毁逻辑。

  • std::call_once 保证初始化只执行一次,比手写锁更轻量
  • std::unique_ptr 可在程序退出前主动 reset(),避免析构顺序不可控
  • 必须用 std::atomicstd::once_flag,别用 bool + 手动 if 判断
class Singleton {
private:
    static std::unique_ptr instance;
    static std::once_flag initFlag;
Singleton() = default;
// ... 其他禁用函数同上

public: static Singleton& getInstance() { std::call_once(initFlag, [] { instance = std::make_unique(); }); return *instance; }

static void destroy() {
    instance.reset();
}

}; std::unique_ptr Singleton::instance = nullptr; std::once_flag Singleton::initFlag;

getInstance() 返回引用还是指针?

返回引用是默认推荐:语义清晰(单例必然存在)、避免空检查、防止用户误调用 delete。但若业务要求“允许未初始化状态”(比如配置未加载前不创建),就必须返回指针并接受 nullptr

注意:一旦返回指针,所有调用点都得做空值判断;而返回引用后,如果构造函数抛异常,getInstance() 后续调用会直接崩溃(C++ 标准规定:静态局部变量初始化异常后,再次访问会重新抛该异常)。

  • 95% 场景用 Singleton& 就够了
  • 若真要指针,用 std::shared_ptr 比裸指针更安全
  • 绝不要返回 Singleton* 并让用户负责生命周期

链接时重复定义 getInstance() 怎么办?

getInstance() 定义放在 .cpp 文件里,而不是头文件中 —— 否则多个源文件包含该头,会触发 ODR(One Definition Rule)违规,链接时报 “multiple definition” 错误。

常见做法是头文件只声明,.cpp 实现;或者用 inline(C++17 起)标记函数,允许在头文件中定义:

  • inline 最省事,但需确认编译器支持 C++17
  • 不用 inline 就必须拆头/实现,否则 Windows MSVC 和 Linux GCC 都会报错
  • 模板类单例天然内联,不受此限,但一般不建议模板化单例
// Singleton.h(C++17)
class Singleton {
public:
    inline static Singleton& getInstance() {
        static Singleton instance;
        return instance;
    }
    // ...
};

单例看似简单,真正难的是边界场景:构造异常、析构顺序、跨 DLL 边界、单元测试重置。多数人只写了“能跑”的版本,却没考虑模块解耦时它成了隐式依赖黑洞。