详解单例模式(c++)

1. 什么是单例模式?

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

种类:懒汉式和饿汉式

2. 单例模式的设计原理

单例模式的核心目标是确保一个类只有一个实例。为实现这一目标,我们需要考虑以下几个关键设计原理:

2.1 私有构造函数(Private Constructor)

通过将构造函数设为私有,我们防止外部直接实例化类,从而强制要求通过指定的方法获取类的实例。

2.2 静态实例(Static Instance)

通过维护一个静态实例,并提供一个静态方法来获取该实例,我们确保在整个应用程序生命周期内只有一个类的实例存在。

2.3 线程安全性(Thread Safety)

在多线程环境下,需要考虑单例模式的线程安全性。通过使用互斥锁等机制,我们可以确保在多线程情况下仍然只有一个实例被创建。

3. 单例模式的实现方式

单例模式有多种实现方式,每种方式都有其优缺点。以下是两种常见的实现方式:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
3.1 懒汉式(Lazy Initialization)
1. 私有构造函数

确保类的跟赋值有关的构造函数以及析构函数是私有的,这样外部无法直接实例化对象。

class T{
    private:
    T(){};//默认构造
    T(const T &)=delete;//拷贝构造
    T& operator=(const T &)=delete;//赋值构造
    T(const T &&)=delete;//移动构造
    T& operator=(const T &&)=delete;//移动赋值构造
    ~T(){};//析构函数
};
  • 为什么要把赋值有关的构造设为私有?
  1. 全局唯一性: 单例模式的目标之一是确保在整个应用程序中只有一个类的实例。通过将构造函数设为私有,防止了外部代码直接实例化对象,从而保障了单例的唯一性。
  2. 控制实例化过程: 将构造函数设为私有允许单例类控制对象的实例化过程。单例类可以在必要时进行懒加载(Lazy Initialization)、初始化一些资源、或执行其他必要的步骤。
  3. 全局访问点: 单例模式通常提供一个全局访问点(如静态方法)来获取单例实例。私有构造函数确保外部代码无法直接通过 new 操作符创建对象,只能通过提供的静态方法获取单例。
  • 为什么要把析构设为私有?
  1. 避免手动删除实例: 如果析构函数是私有的,外部代码无法直接调用 delete 来手动销毁单例实例。这可以防止在程序的其他地方意外删除了单例对象。
  2. 确保唯一性: 确保单例对象只能通过特定的方式进行销毁,例如在程序结束时自动调用析构函数。
  3. 资源管理: 如果单例类管理一些资源(如打开的文件、网络连接等),在析构函数中释放这些资源是一个合理的做法。
2.静态实例

在类内部维护一个静态成员变量,用于保存类的唯一实例。这个静态成员变量需要在类外进行定义和初始化,并在类加载时就创建实例。

class T{
    private:
    static std::mutex _mutex;//懒汉式需要加锁
    static T* instance;//类的唯一实例
    
    private:
    T(){};
    T(const T &)=delete;
    T& operator=(const T &)=delete;
    T(const T &&)=delete;
    T& operator=(const T &&)=delete;
    ~T(){}; 
};
 T* T::instance = nullptr;//类的外部赋值为空
3. 静态方法获取实例以及静态释放示例

提供一个公共的静态方法,用于获取类的实例。在这个方法内部,注册释放函数,并且直接返回创建好的实例(用到Double-Check Locking)。

提供一个私有的静态方法,用于释放类的实例。

class T{
    public:
    //静态获取示例
    static T* GetInstance(){
        //用到了Double-Check Locking(两次检查一次加锁)
        
        if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
         std::lock_guard<std::mutex> lock(_mutex);
            if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
                 instance = new T;
                 atexit(Destructor);//退出时候调用内存释放函数
            }
        }
        return instance;
    }
 //静态释放示例
 private:
 static void Destructor(){
    if(instance!=nullptr)
    delete instance;
 }
    
    private:
    static std::mutex _mutex;
    static T* instance;
    
    private:
    T(){};
    T(const T &)=delete;
    T& operator=(const T &)=delete;
    T(const T &&)=delete;
    T& operator=(const T &&)=delete;
    ~T(){};
    
};
 T* T::instance = nullptr;
  • 如果不使用Double-Check Locking会怎么样?(如下)

    static T* GetInstance(){
        if(instance==nullptr){ 
           instance = new T;
           atexit(Destructor);//退出时候调用内存释放函数
         }
        return instance;
    }
    

    试想一下,如果两个线程同时判断instance为空,那么它们都会去实例化一个T对象,这就变成双例了。所以,我们要解决的是线程安全问题。

    请添加图片描述

如何解决呢?

最容易想到的解决方法就是在方法上加锁,或者是对类对象加锁,程序就会变成下面这个样子

static T* GetInstance(){
    std::lock_guard<std::mutex> lock(_mutex);
    if(instance==nullptr){ 
       instance = new T;
       atexit(Destructor);//退出时候调用内存释放函数
     }
    return instance;
}

这样就规避了两个线程同时创建instance对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

因此用 Double-Check Locking可以很好的规避这个问题

 static T* GetInstance(){
        //用到了Double-Check Locking(两次检查一次加锁)
        if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
         std::lock_guard<std::mutex> lock(_mutex);
            if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
                 instance = new T;
                 atexit(Destructor);//退出时候调用内存释放函数
            }
        }
        return instance;
    }
4.完整测试代码
class T{
    public:
       static T* GetInstance(){
        //用到了Double-Check Locking(两次检查一次加锁)
        
        if(instance==nullptr){// 第一次检查 看是否已经创建 如果已经创建则进入
         std::lock_guard<std::mutex> lock(_mutex);
            if(instance==nullptr){ // 第二次检查,确保只有一个线程创建实例
                 instance = new T;
                 atexit(Destructor);//退出时候调用内存释放函数
            }
        }
        return instance;
    }
 private:
 static void Destructor(){
    if(instance!=nullptr)
    delete instance;
 }
    private:
    //static std::unique_ptr<T> instance;
    static std::mutex _mutex;
    static T* instance;
    T(){};
    T(const T &)=delete;
    T& operator=(const T &)=delete;
    T(const T &&)=delete;
    T& operator=(const T &&)=delete;
    ~T(){};
    
};
 T* T::instance = nullptr;

int main(){
    T* obj1 = T::GetInstance();
    T* obj2 = T::GetInstance();
    if(obj1 == obj2 )
    {
        std::cout<< "这两个是相同的实例" <<std::endl;
    };
}

懒汉式的优缺点:

优点:

  1. 延迟加载: 懒汉式在需要时才创建实例,避免了在程序启动时就创建实例,从而节省了资源。这对于某些资源开销较大的对象是比较合适的。

缺点:

  1. 线程不安全: 懒汉式在多线程环境中可能存在线程安全问题。当多个线程同时检测到实例为nullptr,然后同时创建实例时,就会导致多个实例的产生。
  2. 性能影响: 由于需要在每次获取实例时进行同步(加锁),可能会对性能产生一定的影响。
3.2 饿汉式(Eager Initialization)
代码示例(和懒汉式大致相同)
class T{
    public:
    static T* GetInstance(){
        // std::lock_guard<std::mutex> lock(_mutex); 不需要锁
        if(instance==nullptr)
        {
             instance = new T;
             atexit(Destructor);
        }
        return instance;
    }
 private:
 static void Destructor(){
    if(instance!=nullptr)
    delete instance;
 }
    private:
    //static std::mutex _mutex; 不需要加锁
    static T* instance;
    
    private:
    T(){};
    T(const T &)=delete;
    T& operator=(const T &)=delete;
    T(const T &&)=delete;
    T& operator=(const T &&)=delete;
    ~T(){};
    
};
 T* T::instance = new T();//直接实例化 在类之前
int main(){
    T* obj1 = T::GetInstance();
    T* obj2 = T::GetInstance();
    if(obj1 == obj2 )
    {
        std::cout<< "这两个是相同的实例" <<std::endl;
    };
}

饿汉式的优缺点:

优点:

  1. 线程安全: 饿汉式天生就是线程安全的,因为在类加载时就已经创建好实例,不会存在多个线程同时创建实例的情况。
  2. 简单: 实现简单,不需要考虑线程同步的问题。

缺点:

  1. 资源浪费: 在程序启动时就创建实例,如果该实例占用的资源较大,可能会导致资源浪费。
  2. 不灵活: 无法做到延迟加载,如果在程序运行过程中实例未被使用,也会占用一定的内存。

4.单例的拓展

/ 定义单例模式模板类
template <typename T>
class Singleton {
public:
    // 获取单例实例的静态方法
    static T& GetInstance() {
        static T instance;
        return instance;
    }
protected:
    // 虚析构函数,确保基类可以在需要时正确清理资源
    virtual ~Singleton() {}
    // 隐藏构造函数和赋值操作符,确保只能通过 GetInstance 获取实例
    Singleton() {}
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    Singleton(Singleton&&) = delete;
    Singleton& operator=(const Singleton&&) = delete;
};

// 定义一个名为 Design 的类,继承自 Singleton 模板类
class Design : public Singleton<Design> {
    // 允许 Singleton 模板类访问 Design 的私有成员
    friend class Singleton<Design>;
private:
    // 私有构造函数,确保只能通过 Singleton::GetInstance 获取实例
    Design() {}
    // 私有析构函数,确保只能在需要时销毁实例
    ~Design() {}
public:
    // 一个示例方法,用于显示一条信息
    void Display() {
        std::cout << "Design Singleton Instance" << std::endl;
    }
};

int main() {
    // 获取 Design 单例的实例
    Design& designInstance = Design::GetInstance();

    // 使用单例实例的方法
    designInstance.Display();

    return 0;
}

5. 单例模式的应用场景

单例模式在许多场景中都能够发挥关键作用,确保系统中某个类只有一个实例。以下是一些常见的应用场景:

5.1 资源管理器(Resource Managers)

例如数据库连接池、线程池等资源的管理,确保全局只有一个资源管理器实例。

5.2 配置管理器(Configuration Managers)

在系统中存储配置信息,并通过单例模式确保全局只有一个配置管理器。

5.3 日志记录器(Logging)

在应用程序中记录日志的组件,通过单例模式确保所有的日志信息被统一记录。

5.4 对话框框架(Dialog Framework)

在图形用户界面(GUI)应用程序中,确保只有一个对话框框架管理所有对话框的显示。

6. 总结

(1)单例模式常见的写法有两种:懒汉式饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
例模式确保所有的日志信息被统一记录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值