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

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

相关推荐
Ajiang28247353042 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空2 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
哪 吒5 小时前
最简单的设计模式,抽象工厂模式,是否属于过度设计?
设计模式·抽象工厂模式
Theodore_10225 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’6 小时前
C++ list (链表)容器
c++·链表·list
----云烟----7 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024067 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic7 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it7 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康7 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud