1. 1. C++ 单例模式总结:once_flag + call_once vs static 局部静态对象
    1. 1.1. 一、前言
    2. 1.2. 二、方案一:once_flag + call_once
      1. 1.2.1. 1. 示例代码
      2. 1.2.2. 2. once_flag 和 call_once 是什么?
        1. 1.2.2.1. std::once_flag
        2. 1.2.2.2. std::call_once
      3. 1.2.3. 3. 为什么它线程安全?
      4. 1.2.4. 4. 这种写法的优点
        1. 1.2.4.1. (1)显式表达“只初始化一次”
        2. 1.2.4.2. (2)线程安全
        3. 1.2.4.3. (3)适合复杂初始化逻辑
        4. 1.2.4.4. (4)适合模板封装
      5. 1.2.5. 5. 这种写法的缺点
        1. 1.2.5.1. (1)代码更长
        2. 1.2.5.2. (2)通常需要动态分配对象
        3. 1.2.5.3. (3)把 shared_ptr 暴露出去不一定最优
    3. 1.3. 三、方案二:static 局部静态对象
      1. 1.3.1. 1. 示例代码
      2. 1.3.2. 2. 这种写法为什么线程安全?
      3. 1.3.3. 3. 你可以怎么理解这种线程安全?
      4. 1.3.4. 4. 这种写法的优点
        1. 1.3.4.1. (1)代码最短
        2. 1.3.4.2. (2)线程安全
        3. 1.3.4.3. (3)不需要手动加锁
        4. 1.3.4.4. (4)不需要自己管理堆内存
        5. 1.3.4.5. (5)不容易写错
      5. 1.3.5. 5. 这种写法的缺点
        1. 1.3.5.1. (1)灵活性略差
        2. 1.3.5.2. (2)不适合某些模板基类封装场景
        3. 1.3.5.3. (3)析构时机固定
    4. 1.4. 四、两种写法的本质区别
    5. 1.5. 五、两者都“线程安全”,到底安全在哪?
      1. 1.5.1. 它们保证的是“初始化过程线程安全”
      2. 1.5.2. 但它们不保证“业务操作线程安全”
    6. 1.6. 六、面试里更推荐写哪个?
      1. 1.6.1. 如果只是“手撕单例”
      2. 1.6.2. 如果面试官追问“还有别的写法吗”
    7. 1.7. 七、面试答题模板
      1. 1.7.1. 1. 问:为什么 static 局部静态对象线程安全?
      2. 1.7.2. 2. 问:once_flag + call_once 为什么线程安全?
      3. 1.7.3. 3. 问:那单例是不是就完全线程安全了?
      4. 1.7.4. 4. 问:你更推荐哪种写法?
    8. 1.8. 八、补充:template <typename T> std::shared_ptr<T> Singleton<T>::_instance = nullptr; 是什么意思?
    9. 1.9. 九、最终结论
      1. 1.9.1. once_flag + call_once
      2. 1.9.2. static 局部静态对象
      3. 1.9.3. 面试中最佳策略
    10. 1.10. 十、推荐记忆版
    11. 1.11. 十一、附:两份可直接手撕的代码
      1. 1.11.1. 1. 面试最推荐版本
      2. 1.11.2. 2. 模板 + call_once 版本
    12. 1.12. 十二、结束语

单例模式

C++ 单例模式总结:once_flag + call_once vs static 局部静态对象

一、前言

单例模式是 C++ 面试里非常常见的一道手撕题。

  • 线程安全吗?
  • 为什么线程安全?
  • call_oncestatic 局部静态对象有什么区别?
  • 实战里更推荐哪一种?

这篇文章就专门总结两种最常见、也最容易在面试中出现的写法:

  1. std::once_flag + std::call_once
  2. 函数内 static 局部静态对象(Meyers Singleton)

二、方案一:once_flag + call_once

1. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <memory>
#include <mutex>

template <typename T>
class Singleton {
protected:
Singleton() = default;
~Singleton() = default;

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

static std::shared_ptr<T> _instance;

public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag flag;
std::call_once(flag, []() {
_instance = std::shared_ptr<T>(new T);
});
return _instance;
}
};

template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

2. once_flagcall_once 是什么?

std::once_flag

它是一个“只执行一次”的标记对象。
本身不负责执行业务逻辑,而是配合 std::call_once 使用。

std::call_once

它的作用是:

保证某段代码在多线程环境下只会执行一次。

例如:

1
2
3
4
static std::once_flag flag;
std::call_once(flag, []() {
_instance = std::shared_ptr<T>(new T);
});

意思就是:

  • 多个线程即使同时进入 GetInstance()
  • 也只有一个线程会真正执行 lambda 里的初始化代码
  • 其他线程会等待这次初始化完成
  • 初始化完成后,所有线程拿到的都是同一个对象

3. 为什么它线程安全?

因为标准库已经帮你保证了:

  • 初始化逻辑只执行一次
  • 多个线程并发调用不会创建多个对象
  • 其他线程不会看到“初始化一半”的对象

所以它适合写“懒加载 + 线程安全”的单例。


4. 这种写法的优点

(1)显式表达“只初始化一次”

代码语义很明确,一眼就能看出:这里就是一次性初始化。

(2)线程安全

不需要手写双重检查锁,也不容易写错。

(3)适合复杂初始化逻辑

如果初始化过程很复杂,或者需要多步控制,call_once 的可读性会更好。

(4)适合模板封装

像上面这种写成模板单例基类时,call_once 比较自然。


5. 这种写法的缺点

(1)代码更长

相比 static 局部静态对象,它写法更重。

(2)通常需要动态分配对象

上面的写法里用了:

1
_instance = std::shared_ptr<T>(new T);

这会涉及堆内存分配和智能指针管理。

(3)把 shared_ptr 暴露出去不一定最优

单例更强调“唯一实例”,而 shared_ptr 的语义是“共享所有权”,严格来说不是最贴切。


三、方案二:static 局部静态对象

1. 示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

class Singleton {
private:
Singleton() = default;
~Singleton() = default;

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
};

2. 这种写法为什么线程安全?

关键点在于:

C++11 以后,标准规定:函数内局部静态变量的初始化必须是线程安全的。

也就是说:

1
static Singleton instance;

如果多个线程同时第一次执行到这里:

  • 只有一个线程会真正执行构造
  • 其他线程会等待
  • 构造完成后,所有线程拿到的是同一个对象

所以它天然适合实现单例。


3. 你可以怎么理解这种线程安全?

你可以把它粗略理解成:

1
2
3
4
5
6
7
8
if (还没初始化) {
加锁;
if (还没初始化) {
构造对象;
标记初始化完成;
}
解锁;
}

当然编译器底层不一定真这么写,但逻辑效果类似。
也就是说,不是 static 关键字自己加锁了,而是 编译器和运行时根据 C++11 标准帮你完成了这一层保护


4. 这种写法的优点

(1)代码最短

这是单例模式里最经典、最适合面试手撕的写法。

(2)线程安全

C++11 后标准直接保证局部静态变量初始化线程安全。

(3)不需要手动加锁

不用写 mutex,也不用 call_once

(4)不需要自己管理堆内存

对象直接是静态局部对象,不需要 new,也不需要智能指针。

(5)不容易写错

比双重检查锁更稳,也更容易讲清楚。


5. 这种写法的缺点

(1)灵活性略差

如果你想做非常复杂的初始化控制,call_once 可能更适合。

(2)不适合某些模板基类封装场景

虽然也能做,但不如 call_once 版本自然。

(3)析构时机固定

它的析构通常发生在程序结束阶段,若你想显式控制生命周期,就没那么方便。


四、两种写法的本质区别

对比项 once_flag + call_once static 局部静态对象
初始化方式 显式一次性初始化 局部静态对象首次调用时初始化
线程安全 call_once 保证 C++11 标准保证
代码长度 较长 最短
是否需要动态分配 通常需要 不需要
是否需要智能指针 常见 不需要
生命周期控制 更灵活 相对固定
面试手撕友好度 较好 最好
工程封装能力 更适合模板封装 更适合简单直接场景

五、两者都“线程安全”,到底安全在哪?

这里有一个非常容易被面试官追问的点:

它们保证的是“初始化过程线程安全”

也就是说:

  • 单例对象只会被创建一次
  • 多线程首次访问不会重复构造
  • 不会读到“半初始化对象”

但它们不保证“业务操作线程安全”

例如:

1
2
3
4
5
6
7
8
9
class Singleton {
public:
int cnt = 0;

static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
};

如果多个线程同时做:

1
Singleton::GetInstance().cnt++;

这个 仍然可能不安全

因为:

  • 单例对象创建是线程安全的
  • cnt++ 不是线程安全的

所以要分清:

  1. 单例初始化线程安全
  2. 单例内部成员访问线程安全

这是两回事。


六、面试里更推荐写哪个?

如果只是“手撕单例”

最推荐写:

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
};

原因很简单:

  • 最短
  • 最稳
  • 最容易写对
  • 最容易讲清楚
  • 面试官最容易接受

如果面试官追问“还有别的写法吗”

你可以补充:

还有一种是 std::once_flag + std::call_once,它也能保证线程安全的懒加载初始化。
这种写法更适合模板封装,或者初始化流程比较复杂、希望显式表达“只初始化一次”的场景。

这样回答会显得你不只是会背一个模板,而是真的理解原理。


七、面试答题模板

1. 问:为什么 static 局部静态对象线程安全?

可以这样答:

C++11 以后,函数内局部静态变量的初始化由标准保证线程安全。多个线程同时第一次进入时,只有一个线程会完成初始化,其他线程会等待初始化结束,所以用它实现单例不需要自己额外加锁。


2. 问:once_flag + call_once 为什么线程安全?

可以这样答:

std::call_once 会配合 std::once_flag 保证某段初始化代码在并发环境下只执行一次,所以即使多个线程同时调用 GetInstance(),也只会创建一个实例,其他线程会等待初始化完成后再继续执行。


3. 问:那单例是不是就完全线程安全了?

可以这样答:

不是。它们只能保证单例对象的初始化过程线程安全,不能自动保证单例对象内部成员访问线程安全。比如多个线程同时修改同一个成员变量,仍然需要额外加锁或者用原子变量处理。


4. 问:你更推荐哪种写法?

可以这样答:

如果是面试手撕,我更推荐局部静态对象版本,因为代码最短、最稳、最不容易写错。
如果是工程里要做模板化封装,或者初始化逻辑比较复杂,call_once 版本也很常见。


八、补充:template <typename T> std::shared_ptr<T> Singleton<T>::_instance = nullptr; 是什么意思?

很多人在看模板单例时会卡在这句:

1
2
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

它的意思是:

  • Singleton<T> 是一个类模板
  • 类里面声明了一个静态成员 _instance
  • 这句是在 类外定义这个静态成员变量

也就是说:

1
2
3
4
template <typename T>
class Singleton {
static std::shared_ptr<T> _instance;
};

这里只是声明。

而下面这句才是真正定义:

1
2
template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

对于不同的 T,都会各自生成一份独立的 _instance

例如:

  • Singleton<A> 有一份 _instance
  • Singleton<B> 也有一份 _instance

它们互不干扰。


九、最终结论

once_flag + call_once

适合:

  • 想显式表达“初始化只执行一次”
  • 模板封装
  • 初始化流程较复杂的场景

static 局部静态对象

适合:

  • 面试手撕
  • 简洁直接的单例实现
  • 希望代码最短、最稳、不容易出错的场景

面试中最佳策略

如果面试官让你手撕单例,优先写 局部静态对象版本
然后补充一句:

如果需要模板封装或者更显式的一次性初始化控制,也可以使用 once_flag + call_once 版本。

这样既能写对,也能体现理解深度。


十、推荐记忆版

你只要记住下面这几句话,面试基本就够用了:

  1. static 局部静态对象:C++11 后初始化线程安全
  2. call_once + once_flag:保证初始化逻辑只执行一次
  3. 它们保证的是“初始化线程安全”,不是“业务访问线程安全”
  4. 面试手撕优先写 static 局部静态对象版本
  5. 工程封装或复杂初始化可以考虑 call_once 版本

十一、附:两份可直接手撕的代码

1. 面试最推荐版本

1
2
3
4
5
6
7
8
9
10
11
12
class Singleton {
private:
Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

public:
static Singleton& GetInstance() {
static Singleton instance;
return instance;
}
};

2. 模板 + call_once 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <memory>
#include <mutex>

template <typename T>
class Singleton {
protected:
Singleton() = default;
~Singleton() = default;

Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;

static std::shared_ptr<T> _instance;

public:
static std::shared_ptr<T> GetInstance() {
static std::once_flag flag;
std::call_once(flag, []() {
_instance = std::shared_ptr<T>(new T);
});
return _instance;
}
};

template <typename T>
std::shared_ptr<T> Singleton<T>::_instance = nullptr;

十二、结束语

单例模式本身并不难,真正容易被问住的地方其实是:

  • 为什么线程安全
  • 安全保证的边界是什么
  • call_oncestatic 的区别
  • 模板静态成员为什么要类外定义

把这些点讲清楚,面试里的单例题基本就算稳了。