【设计模式】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 语句,而此时,单例模式就对代码的拓展性产生了影响。

相关推荐
2601_9561219732 分钟前
背包基础篇(01、完全、分组、多重、混合)
c++·算法·动态规划
fpcc1 小时前
跟我学C++中级篇——编译期的条件选择
开发语言·c++
ShineWinsu5 小时前
对于C++:C++20中线程、初始化、Lambda与内存视图等特性的解析
c++·c++20
张张的快乐时光呀!6 小时前
记录使用 MFC 打开文件对话框读取一张图片并做简单的灰度转换算法简单学习过程
c++·mfc
东华万里8 小时前
第41篇 C++类与对象核心知识梳理:从底层原理到面试实战
开发语言·c++·大学生专区
qq_589666058 小时前
C语言、C++与C#的区别详解
c语言·c++·c#
万法若空9 小时前
排列组合恒等式
c++·算法
AI人工智能+电脑小能手9 小时前
大白话说Java设计模式-34-命令模式(业务实战篇)
java·spring·设计模式·命令模式·异步任务·撤销重做·事务封装
老洋葱Mr_Onion10 小时前
【C++】CSP-J初赛模拟卷七错题整理(作者自用)
c++·算法·深度优先
电商API_1800790524711 小时前
速卖通商品采集API技术文章
java·开发语言·c++·api·跨境电商·商品详情