C++设计模式-单例模式

C++设计模式-单例模式


一、概念

单例 Singleton 是设计模式的一种,其特点是只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例:

  • 保证一个类仅有一个实例
  • 提供一个访问它的全局访问点
  • 该实例被所有程序模块所共享

二、应用场景

单例模式的主要作用是确保一个类只有一个实例存在。单例模式可以用在建立目录、数据库连接等需要单线程操作的场合,用于实现对系统资源的控制。

三、定义方式

  • 私有化构造函数,以防止外界创建单例类的对象
  • 不需用拷贝和赋值,在单例模式中,始终只有一个对象
  • 提供一个自身的静态私有成员变量,以指向类的实例
  • 使用一个公有的静态方法获取该实例

四、实现方式

  • 解法一:基础懒汉式单例——单线程安全,多线程不安全
  • 解法二:加锁懒汉式单例——效率低
  • 解法三:对解法二的一个优化
  • 解法四:饿汉式单例——线程安全
  • 解法五:利用C++11的magic static特性实现的Meyers Singleton,安全,推荐

4.1 基础懒汉式单例

示例:

#include <iostream>

/**
 * @brief 基本懒汉式单例(非线程安全)
 * 由于要求只生成一个实例,因此必须把构造函数设为私有函数,以禁止他人创建实例
 * 可以定义一个静态的实例,在需要的时候创建该实例
 * 缺点:多线程情况下,不同线程可能创建出不同的Singleton实例
 */
class Singleton {
 public:
  static Singleton* getInstance() {
    if (instance_ == nullptr) {
      instance_ = new Singleton();
    }
    return instance_;
  }

  static void destroyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
      instance_ = nullptr;
    }
  }

  void todo() { std::cout << "Singleton do ..." << std::endl; }

 private:
  Singleton() = default;                            // 默认构造
  ~Singleton() = default;                           // 默认析构
  Singleton(const Singleton&) = delete;             // 删除拷贝构造
  Singleton& operator=(const Singleton&) = delete;  // 删除拷贝赋值
  Singleton(Singleton&&) = delete;                  // 删除移动构造
  Singleton& operator=(Singleton&&) = delete;       // 删除移动赋值

 private:
  static Singleton* instance_;
};

// 初始化
Singleton* Singleton::instance_ = nullptr;

// test
int main() {
  Singleton::getInstance()->todo();
  return 0;
}

注意:此懒汉式单例,在多线程时,可能会存在多个线程同时getInstance(),导致创建多个不同的实例。

4.2 加锁懒汉式单例

实例:

#include <iostream>
#include <mutex>

/**
 * @brief 加锁懒汉式单例
 * 由于要求只生成一个实例,因此必须把构造函数设为私有函数,以禁止他人创建实例
 * 通过加一个同步锁来保证线程安全
 * 缺点:加锁是一个非常耗时的操作,会影响效率
 */
class Singleton {
 public:
  static Singleton* getInstance() {
    std::lock_guard<std::mutex> lock(mutex_); // 加锁
    if (instance_ == nullptr) {
      instance_ = new Singleton();
    }
    return instance_;
  }

  static void destroyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
      instance_ = nullptr;
    }
  }

  void todo() { std::cout << "Singleton do ..." << std::endl; }

 private:
  Singleton() = default;                            // 默认构造
  ~Singleton() = default;                           // 默认析构
  Singleton(const Singleton&) = delete;             // 删除拷贝构造
  Singleton& operator=(const Singleton&) = delete;  // 删除拷贝赋值
  Singleton(Singleton&&) = delete;                  // 删除移动构造
  Singleton& operator=(Singleton&&) = delete;       // 删除移动赋值

 private:
  static Singleton* instance_;
  static std::mutex mutex_;
};

// 初始化
Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

// test
int main() {
  Singleton::getInstance()->todo();
  return 0;
}

注意:每次getInstance()时都会加锁,影响效率

4.3 对解法二的一个优化

示例:

class Singleton {
 public:
  static Singleton* getInstance() {
    if (instance_ == nullptr) {
      std::lock_guard<std::mutex> lock(mutex_);  // 先判断实例为nullptr后再加锁
      if (instance_ == nullptr) {  // 再次判断
        instance_ = new Singleton();
      }
    }
    return instance_;
  }

  static void destroyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
      instance_ = nullptr;
    }
  }

  void todo() { std::cout << "Singleton do ..." << std::endl; }

 private:
  Singleton() = default;                            // 默认构造
  ~Singleton() = default;                           // 默认析构
  Singleton(const Singleton&) = delete;             // 删除拷贝构造
  Singleton& operator=(const Singleton&) = delete;  // 删除拷贝赋值
  Singleton(Singleton&&) = delete;                  // 删除移动构造
  Singleton& operator=(Singleton&&) = delete;       // 删除移动赋值

 private:
  static std::atomic<Singleton*> instance_;
  static std::mutex mutex_;
};

// 初始化
std::atomic<Singleton*> Singleton::instance_(nullptr);
std::mutex Singleton::mutex_;

// test
int main() {
  Singleton::getInstance()->todo();
  return 0;
}

注意:在getInstance()中先做指针为空的判断,再进行加锁,可以避免每次都加锁,解决效率问题。
但是由于内存reorder可能导致线程并不安全(编译器的问题),new Singleton() 被分成三个步骤执行:

  1. 分配一个Singleton类型对象所需的内存
  2. 在分配的内存初始化Singleton类型对象
  3. 把分配的内存的地址赋给指针instance_

这里步骤1是肯定最先执行的,由但于编译器的问题(C++98),可能会发生 new 操作指令内存reorder,导致步骤2和步骤3的顺序并不一定;假如线程A的执行顺序是 1,3,2,在执行到步骤3时,此时instance_不再为空(nullptr),这时就切换到了线程B,而由于instance_不为空,所以线程B会直接返回instance_,而不会再次创建实例,但这个Singleton类型对象并没有真正被构造,此时如果线程B去访问示例内部的数据时就会出现问题。
解决这个问题,对于C++,在C++11标准中,可以使用原子操作,即atomic。
static std::atomic<Singleton*> instance_;

4.4 饿汉式单例

示例:

#include <iostream>

/**
 * @brief 饿汉式单例(线程安全)
 * 程序刚开始运行时就立即进行初始化,也称为预初始化
 * 缺点:在程序运行时,会占用内存,影响程序性能
 */
class Singleton {
 public:
  static Singleton* getInstance() { 
    return instance_;
  }

  static void destroyInstance() {
    if (instance_ != nullptr) {
      delete instance_;
      instance_ = nullptr;
    }
  }

  void todo() { std::cout << "Singleton do ..." << std::endl; }

 private:
  Singleton() = default;                            // 默认构造
  ~Singleton() = default;                           // 默认析构
  Singleton(const Singleton&) = delete;             // 删除拷贝构造
  Singleton& operator=(const Singleton&) = delete;  // 删除拷贝赋值
  Singleton(Singleton&&) = delete;                  // 删除移动构造
  Singleton& operator=(Singleton&&) = delete;       // 删除移动赋值

 private:
  static Singleton* instance_;
};

// 初始化
Singleton* Singleton::instance_ = new Singleton();

// test
int main() {
  Singleton::getInstance()->todo();
  return 0;
}

注意:在程序开始运行时就申请了内存,占用系统资源

4.5 Meyers Singleton,推荐

利用局部静态变量来实现单例模式。最早由 C++ 大佬、Effective C++ 系列的作者 Scott Meyers 提出,因此也被称为 Meyers’ Singleton

  • 最简洁:不需要额外定义类的静态成员
  • 线程安全,不需要额外加锁
  • 没有烦人的指针

这是 C++11 之后的单例模式最佳实践。

#include <iostream>

/**
 * @brief 利用C++11的 magic static 特性,称为Meyers Singleton,即简单又安全的单例模式实现
 * 核心思想是:利用局部静态变量的特性,来保证单例的延迟初始化和线程安全
 * 静态局部变量,只有在首次定义定义它的函数时才被构造,之后的调用都直接跳过,返回同一实例
 * C++11 标准规定,局部静态变量的初始化过程时线程安全的,这意味着在多线程下,局部静态变量的构造只会执行一次,并且在多线程访问时,也是安全的
 * 如果构造函数异常,局部静态的析构函数也会被执行,保证了单例的完整性
 *
 * 优点:
 * 延迟加载
 * 系统自动调用析构,回收内存
 * 没有加锁和new操作,实现简单,效率高
 * 线程安全
 *
 * gcc4.0及之后的版本支持
 * C++11及之后的标准支持
 */
class Singleton {
 public:
  static Singleton& getInstance() {
    // 局部静态变量,只有在首次定义定义它的函数时才被构造,之后的调用都直接跳过,返回同一实例
    static Singleton instance;
    return instance;
  }

  void todo() { std::cout << "Singleton do ..." << std::endl; }

 private:
  Singleton() = default;                            // 默认构造
  ~Singleton() = default;                           // 默认析构
  Singleton(const Singleton&) = delete;             // 删除拷贝构造
  Singleton& operator=(const Singleton&) = delete;  // 删除拷贝赋值
  Singleton(Singleton&&) = delete;                  // 删除移动构造
  Singleton& operator=(Singleton&&) = delete;       // 删除移动赋值
};

// test
int main() {
  Singleton::getInstance().todo();

  return 0;
}

总结

以上列出了几种常见的单例模式方法和实现,推荐使用最后一种,即Meyers Singleton

  • 12
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值