C++ 【单例模式】

简单介绍

单例模式是一种创建型设计模式 | 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

它也会破坏代码的模块化特性,因为单例模式往往会承担了很多的职责 ,导致与 其他模块 产生过多的耦合

基础理解

单一职责原则:

  • 一个类或模块应该只有一个引起它变化的原因。(确保了只有一个实例)
  • 一个类或模块应该只负责一种功能或行为。(同上)
  • 将不同的功能分离开来,避免将不相关的功能耦合在一起。
    • (单例模式不满足,它同时会有许多职责)

单例模式同时解决了两个问题, 也违反了单一职责原则:

  1. 保证一个类只有一个实例:在需要控制共享资源权限的情况下就十分有用。你创建的所有实例对象都是 获得 第一次创建的实例对象,而不是一个新对象
  2. 为该实例提供一个全局访问节点:和全局变量一样,允许在程序的任何地方访问。 但是它可以保护该实例不被其他代码覆盖。(因为设置了私有)

UML 图

单例 (Singleton) 类声明了一个名为 get­Instance 获取实例的静态方法来返回其所属类的一个相同实例。客户端只允许调用该方法获得实例

单例的构造函数必须对客户端 (Client) 代码隐藏(私有化) 。 调用 获取实例方法必须是获取单例对象的唯一方式。

实现方式

  1. 在类中添加一个私有静态成员变量用于保存单例实例。
  2. 声明一个公有静态构建方法用于获取单例实例。
  3. 在静态方法中实现"延迟初始化"。 该方法会在首次被调用时创建一个新对象, 并将其存储在静态成员变量中。 此后该方法每次被调用时都返回该实例。
  4. 将类的构造函数设为私有。 类的静态方法仍能调用构造函数, 但是其他对象不能调用。

c++ 11 中使用基本的饿汉单例模式是十分简单的, 将GetInstance 设置为static即可,但此单例模式在多线程中会出错,所以需要执行加锁操作.

cpp 复制代码
#include <iostream>  // std::cout
#include <mutex>     // std::mutex
#include <pthread.h> // pthread_create

///  加锁的懒汉式实现  //

class SingleInstance
{

public:
    // 获取单实例对象
    static SingleInstance *&GetInstance(); // 重点

    // 释放单实例,进程退出时调用
    static void deleteInstance();

    // 打印实例地址
    void Print();

private:
    // 将其构造和析构成为私有的, 禁止外部构造和析构
    SingleInstance();
    ~SingleInstance();

    // 将其拷贝构造和赋值构造成为私有函数, 禁止外部拷贝和赋值
    SingleInstance(const SingleInstance &signal);
    const SingleInstance &operator=(const SingleInstance &signal);

private:
    // 唯一单实例对象指针
    static SingleInstance *m_SingleInstance;
    static std::mutex m_Mutex;
};

// 初始化静态成员变量
SingleInstance *SingleInstance::m_SingleInstance = NULL;
std::mutex SingleInstance::m_Mutex;

SingleInstance *&SingleInstance::GetInstance()
{

    //  这里使用了两个 if判断语句的技术称为双检锁;好处是,只有判断指针为空的时候才加锁,
    //  避免每次调用 GetInstance的方法都加锁,锁的开销毕竟还是有点大的。
    if (m_SingleInstance == NULL)
    {
        std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
        if (m_SingleInstance == NULL)
        {
            m_SingleInstance = new (std::nothrow) SingleInstance; // 第一次为空时创建 以后直接返回第一次创建的实例
        }
    }

    return m_SingleInstance;
}

void SingleInstance::deleteInstance()
{
    std::unique_lock<std::mutex> lock(m_Mutex); // 加锁
    if (m_SingleInstance)
    {
        delete m_SingleInstance;
        m_SingleInstance = NULL;
    }
}

void SingleInstance::Print()
{
    std::cout << "我的实例内存地址是:" << this << std::endl;
}

SingleInstance::SingleInstance()
{
    std::cout << "构造函数" << std::endl;
}

SingleInstance::~SingleInstance()
{
    std::cout << "析构函数" << std::endl;
}
///  加锁的懒汉式实现  //

void *PrintHello(void *threadid)
{
    // 主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收
    pthread_detach(pthread_self());

    // 对传入的参数进行强制类型转换,由无类型指针变为整形数指针,然后再读取
    int tid = *((int *)threadid);

    std::cout << "Hi, 我是线程 ID:[" << tid << "]" << std::endl;

    // 打印实例地址
    SingleInstance::GetInstance()->Print();

    pthread_exit(NULL);
    return NULL;
}

#define NUM_THREADS 5 // 线程个数

int main()
{
    pthread_t threads[NUM_THREADS] = {0};
    int indexes[NUM_THREADS] = {0}; // 用数组来保存i的值

    int ret = 0;
    int i = 0;

    std::cout << "main() : 开始 ... " << std::endl;

    for (i = 0; i < NUM_THREADS; i++)
    {
        std::cout << "main() : 创建线程:[" << i << "]" << std::endl;

        indexes[i] = i; // 先保存i的值

        // 传入的时候必须强制转换为void* 类型,即无类型指针
        ret = pthread_create(&threads[i], NULL, PrintHello, (void *)&(indexes[i]));
        if (ret)
        {
            std::cout << "Error:无法创建线程," << ret << std::endl;
            exit(-1);
        }
    }

    // 手动释放单实例的资源
    SingleInstance::deleteInstance();
    std::cout << "main() : 结束! " << std::endl;

    system("pause");
}

应用场景

使用场景就是它的两个特性:

  1. 如果某个类对于所有客户端只有一个可用的实例

单例模式返回的永远只有第一个创建的实例对象,

  1. 更加严格地控制全局变量

除了单例类自己以外, 无法通过任何方式替换的第一个创建的实例。可以修改getInstance 调整单例实例的数量

与其他模式的关系

  1. 外观模式类通常可以转换为单例模式类, 因为在大部分情况下一个外观对象就足够了。

  2. 抽象工厂模式、 生成器模式和原型模式都可以用单例来实现。

  3. 如果你能将对象的所有共享状态简化为一个享元对象, 那么享元模式就和单例类似了。 但这两个模式有两个根本性的不同。

    • 只会有一个单例实体, 但是享元类可以有多个实体, 各实体的内在状态也可以不同。
    • 单例对象可以是可变的。 享元对象是不可变的。

优缺点

优点 缺点
你可以保证一个类只有一个实例。 违反了单一职责原则。 该模式同时解决了两个问题。
你获得了一个指向该实例的全局访问节点。 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等
仅在首次请求单例对象时对其进行初始化。 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。

单例模式被视为一种反模式。 因此它在 C++ 代码中的使用频率正在减少。不过还是有用的.使用好它只需要记住它的两个特点即可

如果有错还望指正。有什么建议也可以留言。
参考文章

相关推荐
源码哥_博纳软云12 分钟前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
学会沉淀。20 分钟前
Docker学习
java·开发语言·学习
ragnwang37 分钟前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
西猫雷婶1 小时前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila1 小时前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾1 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
唐 城2 小时前
curl 放弃对 Hyper Rust HTTP 后端的支持
开发语言·http·rust
码银3 小时前
【python】银行客户流失预测预处理部分,独热编码·标签编码·数据离散化处理·数据筛选·数据分割
开发语言·python
从善若水3 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust