【设计模式】C++单例模式详解

单例模式

⼀个类仅有⼀个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

那么,我们必须保证:该类不能被复制;也不能被公开的创造。

对于 C++ 来说,它的构造函数,拷贝构造函数和赋值函数都不能被公开调用。

单例模式又分为 懒汉模式饿汉模式 ,它们之间各有好处:

  1. 懒汉模式的实例在第一次被引用时才进行初始化,支持延迟加载,资源利用效率更高;但是当资源访问频繁时,资源同步问题(加锁、解锁)会限制并发性能,也就是不支持高并发。

  2. 饿汉模式提前初始化实例,启动时间较长;但是可以避免资源同步(加锁、解锁)带来的性能消耗,后续的响应时间更好。

饿汉模式

在加载类时,对象实例就被创建并初始化,在程序结束时自动销毁,因此,它是线程安全的。

cpp 复制代码
//.h文件 
class Singleton { 
public:   
    static Singleton* GetInstance(){    
        return _Instance; 
    }
  
private:   
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;   
    Singleton& operator = (const Singleton&) = delete; 

private:   
    static Singleton* _Instance; 
}; 

// .CPP文件 
Singleton* Singleton::_Instance = nullptr;  // 类外初始化,必须写

懒汉模式

在 C++11 标准中,静态局部变量 的初始化是线程安全的,因此,可以用以下方式来编写"懒汉"模式:

cpp 复制代码
class Singleton {
public:
    static Singleton& GetInstance() {
        static Singleton instance;  
        return instance;
    }
private:
    Singleton() = default;
    ~Singleton() = default;
    Singleton(const Singleton&) = delete;
    Singleton& operator = (const Singleton&) = delete;
};

更加传统的写法如下,使用 双重检查锁定 来确保线程安全,通过静态成员函数 Destructor 来解决内存泄漏:

cpp 复制代码
//代码实例(线程安全) 
emplate<typename T> 
class Singleton { 
public: 
    static T* getInstance() {       
        if (_instance == nullptr) {   
            lock_guard<mutex> lock(_mutex);   
            if (_instance == nullptr) {     // 双重检查锁定, 确保线程安全
                _instance = new T();
                atexit(Destructor);     // 在程序退出的时候释放资源
            }
        }   
        return _instance; 
    } 
private:    
    // 防止外界构造/拷贝/删除对象
    Singleton() = default;     
    ~Singleton() = default;     
    Singleton(const Singleton&) = delete;
    Singleton& operator = (const Singleton&) = delete;
  
    static void Destructor() {
        if (_instance != nullptr) {
            delete _instance;
            _instance = nullptr;
        }
    }
    
    // 静态指针
    static T* _instance; 
    // 互斥锁
    static mutex _mutex;
}; 

// 初始化静态指针
template<typename T> 
T* Singleton<T>::_instance = nullptr;

// 初始化互斥锁
template<typename T>
mutex Singleton<T>::_mutex;

但是,这种写法可能出现以下问题:

由于 new 操作分为三步:分配内存、返回指针、调用构造函数;如果一个线程在分配内存后返回了指针,但还没有构造对象,另一个线程就尝试访问该对象的情况,就可能会出现未定义行为或错误。

因此,还是推荐使用"静态局部变量"的写法。

单例的应用

需要强调的是,单例只是一种组织全局变量和静态函数的方式

当我们想要拥有应用于某种全局数据集的功能,并且我们想要重复使用时,单例是非常有用的。比如以下场景:

  • 配置管理;

  • 日志记录;

  • 消息队列;

  • 线程池、连接池、内存池、对象池。

但是,单例模式是存在弊端的:

  1. 单例模式会隐藏类之间的依赖关系。

由于单例类不需要显示地创建,也不需要依赖参数传递,在函数中直接调用就好,所以在阅读代码时,需要仔细阅读才能清楚哪些类依赖了单例类。

  1. 单例模式的拓展性较差。

单例类只能创建一个实例,如果哪天需要在代码中创建多个实例,则需要对代码进行较大的改动。

以数据库连接池为例,假设一开始我们将其设计为一个单例类,而后我们发现,有些 SQL 语句的执行效率低下,长时间占用连接资源,因此我们希望再创建一个连接池实例,让它专门处理运行速度较慢的 SQL 语句,而此时,单例模式就对代码的拓展性产生了影响。

相关推荐
娅娅梨5 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控9 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
我爱工作&工作love我17 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
马剑威(威哥爱编程)1 小时前
哇喔!20种单例模式的实现与变异总结
java·开发语言·单例模式
gjh12081 小时前
单例模式和适配器模式的简单介绍
单例模式·适配器模式
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h1 小时前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
好睡凯1 小时前
c++写一个死锁并且自己解锁
开发语言·c++·算法