C++中的单例模式:它是如何底层工作的?经典问题解析

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:为什么单例不仅仅是“一个对象”



如果你曾经在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;


问题:

  1. 非线程安全。 如果两个线程同时调用getInstance(),它们都会看到nullptr并创建两个对象。这违反了单例的不变性。
  2. 内存泄漏。 通过new创建的对象不会自动删除。(虽然实际上进程结束时内存会返还给操作系统,但析构函数不会被调用)。
  3. 静态初始化问题。 如果另一个静态对象在单例初始化之前尝试使用它——会导致未定义行为。


可以添加互斥锁,但这会减慢每次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;<

博客

书籍推荐