# C++ 单例模式的实现
面试官问"怎么实现单例模式",大多数人能写出懒汉式,但追问"线程安全吗""双检锁有什么坑""现代 C++ 推荐哪种",就开始含糊了。
这道题考的不是能不能写出代码,而是你对四种实现方式的演进逻辑和各自的坑是否清楚。把"为什么一步步演进到 Magic Static"讲清楚,面试官就不会继续追了。
# 简要回答
单例模式确保类只有一个实例 + 提供全局访问点。核心手段:构造函数私有化 + 删除拷贝/赋值 + 静态方法返回唯一实例。现代 C++ 推荐用局部静态变量(Magic Static):static Singleton& getInstance() { static Singleton inst; return inst; }——C++11 标准保证局部 static 变量初始化是线程安全的,代码最简洁,自动析构,没有任何坑。
# 详细回答
四种实现的演进逻辑
饿汉式:程序启动时就创建实例(静态成员变量在 main 之前初始化)。天然线程安全,但如果单例很重且可能用不到,就浪费了资源。另外多个饿汉式单例之间的初始化顺序不确定(static initialization order fiasco)。
懒汉式:第一次调用 getInstance() 时才创建。解决了资源浪费问题,但多线程下两个线程同时判断 instance == nullptr 会创建两个实例——线程不安全。
双检锁(DCLP):在懒汉式基础上加锁。第一次检查避免每次都加锁(性能),加锁后第二次检查避免重复创建。但在 C++11 之前,由于指令重排和内存可见性问题,DCLP 实际上是有 bug 的。C++11 之后需要配合 std::atomic 或 std::call_once 才能正确实现。
Magic Static(C++11 推荐):函数内的局部 static 变量,C++11 标准规定其初始化必须是线程安全的(编译器负责加锁)。代码只需一行,没有手动锁、没有指针、没有内存泄漏(程序结束时自动析构)。这是现代 C++ 的标准答案。
为什么 Magic Static 是最优解
线程安全由编译器保证,不需要程序员操心。返回引用而非指针,不存在 delete 的问题。程序结束时自动调用析构函数,资源正确释放。代码极简,不容易写错。唯一的"缺点"是不能控制销毁顺序——但绝大多数场景不需要。

# 知识拓展
Q:单例模式有什么缺点?什么时候不该用?
单例本质是全局状态,会导致:单元测试困难(全局状态难隔离)、隐藏依赖关系(调用方看不出依赖了单例)、违反单一职责(既管业务又管自己的生命周期)。如果可以用依赖注入替代,优先不用单例。真正适合的场景:日志器、配置管理器、线程池、连接池——这些天然是全局唯一且生命周期贯穿整个程序的。
Q:双检锁在 C++11 之前为什么有 bug?
instance = new Singleton() 这一行实际上分三步:分配内存、调用构造函数、赋值指针。编译器/CPU 可能重排为:分配内存→赋值指针→调用构造函数。这时另一个线程看到 instance != nullptr 就直接返回了,但对象还没构造完——未定义行为。C++11 的 std::atomic 和内存序解决了这个问题,但代码很复杂,不如直接用 Magic Static。
Q:饿汉式的初始化顺序问题是什么?
如果单例 A 的构造函数里用到了单例 B,而 B 还没初始化(不同编译单元的 static 变量初始化顺序未定义),就会崩溃。Magic Static 天然避免了这个问题——谁先被调用谁先初始化,顺序由调用链决定。
Q:单例怎么正确销毁?
Magic Static 方式程序结束时自动析构,不需要手动管理。如果用指针式(饿汉/懒汉/双检锁),需要额外的销毁机制(如 atexit 注册回调),否则内存泄漏。这也是推荐 Magic Static 的原因之一。
评论
验证登录状态...