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++ 代码中的使用频率正在减少。不过还是有用的.使用好它只需要记住它的两个特点即可

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

相关推荐
晨曦_子画4 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
Black_Friend13 分钟前
关于在VS中使用Qt不同版本报错的问题
开发语言·qt
发霉的闲鱼29 分钟前
MFC 重写了listControl类(类名为A),并把双击事件的处理函数定义在A中,主窗口如何接收表格是否被双击
c++·mfc
小c君tt31 分钟前
MFC中Excel的导入以及使用步骤
c++·excel·mfc
希言JY37 分钟前
C字符串 | 字符串处理函数 | 使用 | 原理 | 实现
c语言·开发语言
残月只会敲键盘37 分钟前
php代码审计--常见函数整理
开发语言·php
xianwu54337 分钟前
反向代理模块
linux·开发语言·网络·git
xiaoxiao涛39 分钟前
协程6 --- HOOK
c++·协程
ktkiko1143 分钟前
Java中的远程方法调用——RPC详解
java·开发语言·rpc
y5236481 小时前
Javascript监控元素样式变化
开发语言·javascript·ecmascript