引言:为什么单例不仅仅是“一个对象”
如果你曾经在Stack Overflow上寻找过这个问题的答案:“如何实现单例设计模式?”,那么你一定见过成千上万的投票、几十种不同的实现方案以及评论中激烈的争论。这个问题之所以成为经典,是因为单例是最容易理解、但实现起来却最棘手的模式之一。
在这篇文章中,我们不仅仅展示如何用C++编写单例。我们将深入底层:剖析静态内存是如何工作的,什么是初始化保证,为什么朴素的方法会损害性能,以及最终的方案(Meyers单例)是如何成为事实上的行业标准的。
如果你正在准备面试,或者想更深入地理解C++,这篇文章就是为你准备的。让我们开始吧!
什么是单例?简要回顾
单例是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点。
看起来似乎很简单?但在C++中,有一些细微之处:
- 首次创建对象时的线程安全性。
- 生命周期管理(对象何时被销毁?)。
- 防止拷贝和赋值。
- 多线程问题(数据竞争)。
让我们来看看实现方案的演变——从朴素到工业级。
朴素实现:问题出在哪里?
最简单的方法是将构造函数设为私有,并存储一个静态指针:
class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { instance = new Singleton(); } return instance; }
void doSomething() { // 逻辑 }
private: Singleton() {} // 私有构造函数 Singleton(const Singleton&) = delete; // 禁止拷贝 Singleton& operator=(const Singleton&) = delete; // 禁止赋值
static Singleton* instance;};
// 静态成员的定义Singleton* Singleton::instance = nullptr;问题:
- 非线程安全。 如果两个线程同时调用
getInstance(),它们都会看到nullptr并创建两个对象。这违反了单例的不变性。 - 内存泄漏。 通过
new创建的对象不会自动删除。(虽然实际上进程结束时内存会返还给操作系统,但析构函数不会被调用)。 - 静态初始化问题。 如果另一个静态对象在单例初始化之前尝试使用它——会导致未定义行为。
可以添加互斥锁,但这会减慢每次getInstance()的调用速度,即使对象已经创建。这很糟糕。
双重检查锁定(DCLP):几乎正确,但有风险
一种流行的优化方法是检查指针两次:第一次不加锁,第二次加锁。这被称为双重检查锁定模式。
#include <mutex>
class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { // 第一次检查(不加锁) std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // 第二次检查(加锁) instance = new Singleton(); } } return instance; }
private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;
static Singleton* instance; static std::mutex mutex;};
Singleton* Singleton::instance = nullptr;std::mutex Singleton::mutex;<