当前位置: 首页 > article >正文

C++ 实现单例模式

单例模式

单例模式确保一个类只有一个实例,并提供一个全局访问点

创建单一实例

怎么让某个类只能创建一个实例?

思路:将类的构造函数私有,然后提供一个静态方法访问对象。调用类内成员函数需要对象,但我们又无法创建出对象,所以要将该接口函数声明为静态函数,这样就可以在类外使用类名调用。

class Singleton {
 public:
  static Singleton* GetInstance() {
    if (_uniqueInstance == nullptr) {
      _uniqueInstance = new Singleton;
    }
    return _uniqueInstance;
  }
  
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

 private:
  Singleton() = default;
  static Singleton* _uniqueInstance;
};

Singleton* Singleton::_uniqueInstance = nullptr;

我们用一个指针保存创建的单例对象,并在第一次调用 GetInstance 时创建对象,以后就直接返回该单例对象。

多线程下的问题

那么问题来了,如果一个线程判断指针为空,线程创建单例对象。另一个线程在上一个线程创建返回之前,同样进行了判断也得到了指针为空的结果,同样进入创建对象。此时,一个单例对象竟被创建了两次。我们可以采用加锁的方式,让多线程互斥地访问该部分。

class Singleton {
 public:
  static Singleton* GetInstance()  {
    _mtx.lock();
    if (_uniqueInstance == nullptr) {
      _uniqueInstance = new Singleton;
    }
    _mtx.unlock();
    return _uniqueInstance;
  }
  
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

 private:
  Singleton() = default;
  static Singleton* _uniqueInstance;
  static mutex _mtx;
};

Singleton* Singleton::_uniqueInstance = nullptr;
mutex Singleton::_mtx;

此时又有新的麻烦了,明明我们只需要在第一次进入时互斥,后续访问就不再需要了。采用加锁的方式将大大降低程序的运行效率,这在性能要求高的程序中是不可容忍的。

双加锁

一个比较常见的解决方式是双加锁,首先检查实例是否已经创建了,如果还没创建,才进行互斥控制。这样一来,就只有第一次会进行互斥控制。

static Singleton* GetInstance()  {
  // 使用 double-check 方式加锁,保证效率和线程安全
  if (_uniqueInstance == nullptr) {
    _mtx.lock();
    if (_uniqueInstance == nullptr) {
      _uniqueInstance = new Singleton;
    }
    _mtx.unlock();
  }
  return _uniqueInstance;
}

急切创建实例

如果程序总是创建并使用单例实例,或者在创建和运行时方面的负担不太繁重,你可以急切创建此单例。

class Singleton {
 public:
  static Singleton* GetInstance() {
    return &_instance;
  }
  
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

 private:
  Singleton() = default;
  static Singleton _instance;
};

Singleton Singleton::_instance;

如果是 Java,上述代码形式没什么问题。JVM 在加载这个类时马上创建此唯一的单例实例。JVM 保证在任何线程访问 uniqueInstance 静态变量之前,一定先创建此实例。

但在 C++ 中还有潜在的问题,简单点说就是当你调用 GetInstance() 获得了对象的引用,你打算用这个单例对象初始化另一个单例对象,就有可能导致错误,下文将解释原因。

non-local static 对象初始化次序问题

函数内的 static 对象称为 local static 对象,其他 static 对象称为 non-local static 对象。

编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件。

如果某编译单元内的某个 non-local static 对象的初始化动作使用了另一编译单元内的某个 non-local static 对象,它所用到的这个对象可能尚未被初始化,因为 C++ 对「定义于不同编译单元内的 non-local static 对象」的初始化次序并无明确定义

一个小小的设计便可以解决这个问题。唯一需要做的是:将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。

这个手法的基础在于:C++ 保证,函数呢的 local static 对象会在「该函数被调用期间」「首次遇上该对象的定义式」时被初始化。

class Singleton {
 public:
  static Singleton& getInstance() {
    static Singleton inst;
    return inst;
  }
  
  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  
 private:
  Singleton() = default;
};

http://www.kler.cn/a/226840.html

相关文章:

  • 代码对齐自动缩进排版代码格式化美化代码快捷键ShortcutKeyHotKey
  • [Unity Sentis] Unity Sentis 详细步骤工作流程
  • 速盾:服务器接入免备案CDN节点的好处有哪些
  • Jmeter学习系列之三:测试计划详细介绍
  • mysql 正则表达式用法(一)
  • 异步编程Completablefuture使用详解----进阶篇
  • Kotlin:用源码来深入理解 ‘StateFlow和SharedFlow的区别和联系‘
  • k8s-实战——kubeadm安装v1.29.1
  • Windows 共享文件 netlogon和sysvol的作用
  • 深入理解并测试HttpResponse —— 关键知识和实践
  • 109.乐理基础-五线谱-五线谱的附点、休止符、连线、延音线
  • 获取指定进程中的数据
  • Vue3学习记录(二)--- 组合式API之计算属性和侦听器
  • 学习数据结构的第一天
  • 代码随想录算法训练营第四十一天|122. 买卖股票的最佳时机 II
  • 专业139总分400+南昌大学811信号与系统考研经验电子信息与通信工程集成电路
  • Redis核心技术与实战【学习笔记】 - 16.Redis 缓存异常:缓存和数据库不一致
  • 红日三打靶!!!
  • 力扣hot100 编辑距离 多维DP
  • 力扣刷题之旅:启程篇(二)