单例模式(Singleton)
一、什么是单例模式(Singleton)
定义:
单例模式保证一个类在程序运行期间 只有一个实例 ,并提供一个 全局访问点 来获取该实例。
核心目标:
- 控制对象的创建(只创建一次)
- 全局可访问
- 避免重复实例造成的资源浪费或状态冲突
重要理解单例模式的本质不是:
"全局只能有一个对象"
而是:
"在程序生命周期内,某类资源在逻辑上必须全局唯一,并由该类自己负责创建与访问"
换句话说:
- 唯一性是业务语义,不是技术约束
- Singleton 只是实现"唯一性"的一种方式
二、单例模式的基本结构
一个典型的单例类具备以下特征:
- 构造函数私有化
- 拷贝构造、赋值运算符禁用
- 通过静态方法返回唯一实例
cpp
class Singleton {
public:
static Singleton& instance();
private:
Singleton(); // 构造函数私有
~Singleton();
Singleton(const Singleton&) = delete; // 禁止拷贝
Singleton& operator=(const Singleton&) = delete; // 禁止赋值
};
三、常见实现方式
1. 饿汉式(Eager Initialization)
程序启动时就创建实例
cpp
class Singleton {
public:
static Singleton& instance() {
return instance_;
}
private:
Singleton() {}
static Singleton instance_;
};
// 类外定义
Singleton Singleton::instance_;
优点
- 实现简单
- 天然线程安全
缺点
- 即使不用也会创建
- 可能增加启动时间
- 静态初始化顺序问题(跨编译单元)
2. 懒汉式(非线程安全,不推荐)
cpp
class Singleton {
public:
static Singleton* instance() {
if (!instance_) {
instance_ = new Singleton();
}
return instance_;
}
private:
Singleton() {}
static Singleton* instance_;
};
Singleton* Singleton::instance_ = nullptr;
问题严重:
- 多线程下可能创建多个实例
- 内存泄漏风险
- 不推荐使用
3. 双重检查锁(DCLP,C++11 前后差异)
cpp
#include <mutex>
class Singleton {
public:
static Singleton* instance() {
if (!instance_) {
std::lock_guard<std::mutex> lock(mutex_);
if (!instance_) {
instance_ = new Singleton();
}
}
return instance_;
}
private:
Singleton() {}
static Singleton* instance_;
static std::mutex mutex_;
};
问题
- C++11 之前 存在指令重排问题
- 实现复杂
- 可读性差
现代 C++ 中不推荐
4. Meyers Singleton(推荐方式)
C++11 之后的标准解法
cpp
class Singleton {
public:
static Singleton& instance() {
static Singleton instance; // 局部静态变量
return instance;
}
private:
Singleton() {}
};
推荐原因
-
C++11 保证:
- 局部静态变量初始化线程安全
-
懒加载
-
无需手动加锁
-
自动析构(程序结束)
这是目前最优雅、最安全的实现
5. 智能指针 + call_once
cpp
#include <memory>
#include <mutex>
class Singleton {
public:
static Singleton& instance() {
std::call_once(initFlag, &Singleton::init);
return *instance_;
}
private:
Singleton() {}
static void init() {
instance_.reset(new Singleton());
}
static std::unique_ptr<Singleton> instance_;
static std::once_flag initFlag;
};
std::unique_ptr<Singleton> Singleton::instance_;
std::once_flag Singleton::initFlag;
适合场景
- 需要精细控制初始化流程
- 单例生命周期复杂
6.工程级增强:接口 + Singleton(强烈推荐)
问题:单例导致"不可测试"
cpp
foo() {
Config::instance().maxIter(); // 隐式依赖
}
解法:接口隔离
cpp
class IConfig {
public:
virtual ~IConfig() = default;
virtual int maxIter() const = 0;
};
cpp
class Config final : public IConfig {
public:
static Config& instance() {
static Config inst;
return inst;
}
int maxIter() const override { return max_iter_; }
private:
int max_iter_ = 10;
};
cpp
void run(const IConfig& cfg) {
std::cout << cfg.maxIter() << std::endl;
}
测试时
cpp
class MockConfig : public IConfig {
public:
int maxIter() const override { return 1; }
};
四、单例模式的常见陷阱
1. 静态初始化顺序问题(Static Initialization Order Fiasco)
cpp
// a.cpp
Singleton& s = Singleton::instance();
// b.cpp
static Singleton singleton;
解决方案:
- 使用 函数内局部静态对象(Meyers Singleton)
2. 析构顺序问题
- 单例在程序结束时析构
- 可能被其他静态对象使用
建议:
- 避免在析构阶段访问单例
- 或使用"永不析构"的方式(new + 不 delete)
3. 隐式全局变量问题
单例本质上是:
"受控的全局变量"
可能导致:
- 强耦合
- 难以测试
- 难以并行化
五、单例模式的适用场景
1. 必要条件(满足 ≥ 2 条才考虑)
| 条件 | 解释 |
|---|---|
| 资源唯一 | OS / GPU / 文件 / 端口 |
| 全局一致性 | 配置、日志、统计 |
| 生命周期长 | 从 main 到 exit |
| 跨模块共享 | 多子系统都要用 |
适合场景:
- 日志系统(Logger)
- 配置管理(Config)
- 线程池
- 资源管理器(GPU、文件句柄)
- 全局状态只允许唯一实例
2. 不要用 Singleton 的典型场景
| 场景 | 为什么是灾难 |
|---|---|
| 算法对象 | 无法并行、多实例 |
| Solver / Optimizer | 测试和调参困难 |
| 业务对象 | 隐式依赖 |
| 状态机 | 无法回滚 |
经验法则:
如果你将来可能想同时创建两个对象 → 不要用 Singleton
六、Singleton 的生命周期与初始化陷阱
错误 1:构造函数做重活
cpp
Config() {
load("config.yaml"); //
}
问题
- 初始化顺序不可控
- 异常难处理
正确做法
cpp
void init(const std::string& path) {
load(path);
}
cpp
int main() {
Config::instance().init("config.yaml");
}
错误 2:单例互相依赖(静态初始化地狱)
cpp
Logger::instance().log(Config::instance().path());
解法
- 延迟调用
- 显式初始化顺序
- 不在构造函数中访问其他 Singleton
七、线程安全 ≠ 单例安全
1. 常见误区
cpp
Config::instance().set("x", 1); // 多线程
单例的"创建"是安全的,
但"状态访问"不一定安全
2. 正确方式
cpp
class Config {
public:
int get() const {
std::shared_lock lock(mtx_);
return value_;
}
void set(int v) {
std::unique_lock lock(mtx_);
value_ = v;
}
private:
mutable std::shared_mutex mtx_;
int value_;
};
八、Singleton 的替代方案
| 方案 | 何时用 |
|---|---|
| 依赖注入(DI) | 大型系统 |
| Context 对象 | 算法工程 |
| Namespace + static | 无状态工具 |
| Factory + Registry | 插件系统 |
如果只是"方便访问",那你并不需要 Singleton
七、综合示例
1.简单示例
cpp
class Singleton {
public:
static Singleton& instance() {
static Singleton instance;
return instance;
}
void doSomething() {}
private:
Singleton() = default;
~Singleton() = default;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
};
2.工程示例
场景:在SLAM / 算法工程中的配置 + 算法模块
Config(唯一)
cpp
class Config {
public:
static Config& instance();
int maxIterations() const;
private:
int max_iter_;
};
Optimizer(不依赖 Singleton)
cpp
class Optimizer {
public:
explicit Optimizer(const IConfig& cfg)
: max_iter_(cfg.maxIterations()) {}
void solve();
private:
int max_iter_;
};
cpp
int main() {
auto& cfg = Config::instance();
cfg.init("config.yaml");
Optimizer opt(cfg);
opt.solve();
}
Singleton 只出现在 main / infra 层
面试问题
1. 什么是单例模式?为什么要用它?
考察点
- 是否理解"唯一性语义"
- 是否知道它不是"全局变量"
优秀回答要点
- 保证某类资源在程序中逻辑唯一
- 控制实例创建与访问
- 常用于日志、配置、硬件资源
错误回答
"就是全局只能有一个对象"
2. C++11 之后最推荐的单例实现方式是什么?为什么?
标准答案
Meyers Singleton(函数内 static)
理由
- C++11 保证线程安全
- 延迟初始化
- 无锁
- 实现简单
cpp
static T instance;
3. 单例和全局变量的本质区别是什么?
关键区别
| 方面 | 全局变量 | Singleton |
|---|---|---|
| 创建控制 | ❌ 无 | ✅ 有 |
| 生命周期 | 编译期 | 运行期 |
| 封装性 | 差 | 好 |
| 可替换性 | 差 | 较好 |
4. 为什么要删除拷贝和移动构造?
考察点
- 是否理解"唯一性破坏"
回答要点
- 防止通过拷贝 / 移动产生第二个实例
- 编译期约束优于运行期
cpp
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
5. Meyers Singleton 是如何保证线程安全的?
优秀回答
- C++11 标准规定:
函数内静态变量初始化是线程安全的 - 编译器保证只初始化一次
- 通常使用内部原子 / guard
6. 双重检查锁(DCLP)为什么不推荐?
答题要点
- 代码复杂
- 易出错
- 早期内存重排序问题
- C++11 后完全没必要
加分点:
提到 memory reordering / std::atomic / fence
7. Singleton 最大的工程问题是什么?
核心答案
隐藏依赖、强耦合、难测试
展开点:
- 隐式依赖不可见
- 无法注入 Mock
- 多线程状态管理复杂
8.如何让 Singleton "可测试"?
优秀回答
- 接口抽象(Interface)
- 依赖注入
- Singleton 只作为默认实现
cpp
void foo(const IConfig& cfg);
9. 为什么不建议在算法模块中使用 Singleton?
答题要点
- 算法通常需要多实例
- 不利于并行 / 多数据集
- 隐藏状态影响收敛和调试
SLAM / 优化器 面试加分题
10. Singleton 和依赖注入(DI)如何取舍?
对比思路
| 场景 | 更合适 |
|---|---|
| 小项目 / infra | Singleton |
| 大系统 / 可测试 | DI |
| 算法核心 | DI |
| 工具模块 | Singleton |
11. Singleton 的构造和析构顺序有什么问题?
考察点
- 静态初始化顺序问题
回答要点
- 不同翻译单元顺序未定义
- 避免在构造 / 析构中访问其他单例
- 显式 init / shutdown
12. 单例一定是线程安全的吗?
正确答案
不一定
说明:
- 创建是线程安全的
- 成员访问未必安全
- 需自行加锁
13. 如何实现"可重置"的单例?为什么不推荐?
答题要点
- 可通过指针 + reset
- 破坏单例语义
- 多线程风险极高
- 测试专用,生产禁用
14. 如何在插件系统中避免 Singleton?
优秀回答
- Factory + Registry
- Context 传递
- 生命周期由上层控制
15. 单例与 RAII 的关系?
加分点
- Singleton 是对象管理
- RAII 是资源管理
- 单例常配合 RAII 使用(如 Logger / File)
16. 请说一个你用 Singleton 踩过的坑
面试官想听
- 初始化顺序问题
- 单元测试困难
- 并发 bug
真实经历 > 完美答案
17 手写线程安全单例(5 分钟)
cpp
class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
private:
Singleton() = default;
};
18. 改造代码:去掉 Singleton 滥用
考察点
- 架构能力
- 依赖反转
总结回答
"Singleton 不是为了'方便访问',
而是为了'表达唯一性语义',
如果只是方便,我会用依赖注入。"
面试官视角总结
| 等级 | 表现 |
|---|---|
| 初级 | 会写单例 |
| 中级 | 知道什么时候不该用 |
| 高级 | 会替代 Singleton |
| 架构 | 能限制 Singleton 的影响范围 |