• 设计模式_单例模式(C++)



    单例模式介绍

    单例 Singleton 是设计模式的一种,其特点只提供唯一一个类的实例,具有全局变量的特点,在任何位置都可以通过接口获取到那个唯一实例;


    常见的运用场景

    • 设备管理器,系统中可能有多个设备,但是只有一个设备管理器,用于管理设备驱动;
    • 数据池,用来缓存数据的数据结构,需要在一处写,多处读取或者多处写,多处读取;
    • 回收站,在整个系统运行过程中,回收站一直维护着仅有的一个实例。
    • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。’
    • 网站的计数器,一般也是采用单例模式实现,否则难以同步。

    实现要点

    • 全局只有一个实例:static 特性,同时禁止用户自己声明并定义实例(把构造函数设为 private)
    • 线程安全
    • 禁止赋值和拷贝
    • 用户通过接口获取实例:使用 static 类成员函数

    单例模式的写法(C++)

    单例模式的写法分为两种,区别在于实例化的时机.

    • 饿汉式(Eager initialization)

    像一个饿汉一样,不管需不需要用到实例都要去创建实例,即在类产生的时候就创建好实例,这是一种空间换时间的做法.作为一个饿汉而言,体现了它的本质——“我全都要”.


    • 优缺点

    优点:

    1. 避免了线程同步问题.
    2. 线程安全在类加载的同时已经创建好一个静态对象,调用时反应速度快.

    缺点:

    1. 来类装载的时候就完成了实例化,没有达到Lazy Loading的效果.
    2. 如果从始至终未使用过这个实例,则会造成实例的浪费.

    • 普遍方式

    代码示例:

    #pragma once
    
    #include <iostream>
    
    class Singleton {
     //构造器私有化
    private:
    	 Singleton() {}
    	 Singleton(const Singleton&)
    	 Singleton& operator=(const Singleton&);
    //想要这个实例在程序运行的整个过程中都存在,所以我们不允许实例自己主动调用析构函数释放对象.
        ~Singleton(){) 
        
    	 static Singleton instance;     //方式一:类对象
      	 static Singleton* instance =new Singleton(); //方式二:类对象的指针
    //提供一个公有的静态方法,返回实例对象.
    public :
    	 static Singleton getInstance()  { return &instance; } //方式一:返回类对象
    	 static Singleton* Singleton::getInstance(){ return instance; }//方式二:返回类对象指针
    	 static void deleteInstance()	 { delete instance; } //方式二:用来销毁对象的指针的实例
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    • shared_ptr与自定义删除器方式

    代码示例:

    #pragma once
    
    #include <iostream>
    #include <memory>
    using namespace std;
    class Singleton{
    private:
        Singleton(){
            cout <<__TIME__<<":" << __FUNCTION__<< endl;
        }
        Singleton(const Singleton&);
        Singleton& operator=(const Singleton&);
        ~Singleton(){
            cout <<__TIME__<<":" <<__FUNCTION__ << endl;
        }
        static void DestroyInstance(Singleton*);    //自定义一个释放实例的函数
        static shared_ptr<Singleton> instance;  //这是我们的单例对象,它是一个类对象的指> >     
    public:
        static shared_ptr<Singleton> getInstance();
        shared_ptr<Singleton> Singleton::instance(new Singleton(),Singleton::DestroyInstance); 
        shared_ptr<Singleton> Singleton::getInstance(){ return instance;  }
        void Singleton::DestroyInstance(Singleton*){ cout <<__TIME__<<":" << __FUNCTION__<< endl;}
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 懒汉式(Lazy-Initialization)

    像一个懒汉一样,需要用到创建实例了程序再去创建实例,不需要创建实例程序就“懒得”去创建实例,这是一种时间换空间的做法.
    懒汉式的方法是直到使用时才实例化对象,也就说直到调用get_instance() 方法的时候才 new 一个单例的对象, 如果不被调用就不会占用内存。


    • 优缺点

    优点:
    单例对象的生成是在用户需要使用的时候再去构造,提高了应用的启动速度.
    缺点:
    需要为保证线程安全增加额外的开销.


    • 线程安全、内存安全的懒汉式单例 (智能指针,锁)

    优点:
    1.基于 shared_ptr, 用了C++比较倡导的 RAII思想,用对象管理资源,当 shared_ptr 析构的时候,new 出来的对象也会被 delete掉,以此避免内存泄漏.
    2.加了锁,使用互斥量来达到线程安全。使用两个 if判断语句的技术称为双检锁,好处是,只有判断指针为空的时候才加锁,避免每次调用 get_instance的方法都加锁,锁的开销毕竟还是有点大的.
    缺点:
    1.使用智能指针会要求用户也得使用智能指针,非必要不应该提出这种约束.
    2.使用锁也有开销; 同时代码量也增多了,实现上我们希望越简单越好.
    3.在某些平台(与编译器和指令集架构有关),双检锁会失效.


    代码示例:

    #pragma once
    
    #include <iostream>
    #include <memory> // shared_ptr
    #include <mutex>  // mutex
    
    class Singleton{
    public:
    	typedef std::shared_ptr<Singleton> Ptr;
     	Singleton(Singleton&)=delete;
     	Singleton& operator=(const Singleton&)=delete;
    	 ~Singleton(){	std::cout<<"destructor called!"<<std::endl; }
    
     	static Ptr get_instance()
    	 {
            // "double checked lock"
            if(m_instance_ptr==nullptr)
            {
                std::lock_guard<std::mutex> lk(m_mutex);
                if(m_instance_ptr == nullptr)
                {
                  m_instance_ptr = std::shared_ptr<Singleton>(new Singleton);
                }
            }
            return m_instance_ptr;
        }
    private:
        Singleton() {std::cout<<"constructor called!"<<std::endl;}
        static Ptr m_instance_ptr;
        static std::mutex m_mutex;
    };
         
    // initialization static variables out of class
        Singleton::Ptr Singleton::m_instance_ptr = nullptr;
        std::mutex Singleton::m_mutex;
      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    • Magic Static式单例(局部静态变量)

    这种方法又叫做 Meyers’ SingletonMeyer’s的单例, 是著名的写出《Effective C++》系列书籍的作者 Meyers 提出的。所用到的特性是在C++11标准中的Magic Static特性:

    If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
    如果当变量在初始化的时候,并发同时进入声明语句,并发线程将会阻塞等待初始化结束。
    这样保证了并发线程在获取静态局部变量的时候一定是初始化过的,所以具有线程安全性。
    C++静态变量的生存期 是从声明到程序结束,这也是一种懒汉式.


    优点:
    1.通过局部静态变量的特性保证了线程安全 (C++11, GCC > 4.3, VS2015支持该特性);
    2.不需要使用共享指针,代码简洁;
    3.注意在使用的时候需要声明单例的引用 Single& 才能获取对象。


    代码示例:

    #pragma once
    
    #include <iostream>
    using namespace std;
    
    class Singleton{
    public:
    	~Singleton(){cout <<__TIME__<<":"<< __FUNCTION__<< endl;}
    
    	Singleton(const Singleton&)=delete;
    	Singleton& operator=(const Singleton&)=delete;
    
    	static Singleton& get_instance()
    	{
    		static Singleton instance;
    		return instance;
    	}
    private:
    Singleton(){cout <<__TIME__<<":" <<__FUNCTION__<< endl;}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    单例类模版(C++)

    • CRTP 奇异递归模板模式实现

    基类模板的实现要点

    1.构造函数需要是 protected,这样子类才能继承.
    2.使用了奇异递归模板模式CRTP(Curiously recurring template pattern).
    3.get instance 方法和 static local方法一个原理.
    4.在这里基类的析构函数可以不需要 virtual ,因为子类在应用中只会用 Derived 类型,保证了析构时和构造时的类型一致.


    • 代码示例

    #pragma once
    // brief: a singleton base class offering an easy way to create singleton
    #include <iostream>
    
    template<typename T> class Singleton{ 
    public:
        static T& get_instance()
        {
            static T instance;
            return instance;
        }
        virtual ~Singleton()
        {
            std::cout<<"destructor called!"<<std::endl;
        }
        Singleton(const Singleton&)=delete;
        Singleton& operator =(const Singleton&)=delete;
    protected:
        Singleton()
        {
            std::cout<<"constructor called!"<<std::endl;
        }
    };
     /********************************************************************************************/
     // Example:
     //1.friend class declaration is requiered!
     // 2.constructor should be private
    
    class DerivedSingle:public Singleton<DerivedSingle>
    {   
     // !!!!attention!!!    
     // needs to be friend in order to   
     // access the private constructor/destructor    
    		friend class Singleton<DerivedSingle>; 
    public:    
    		DerivedSingle(const DerivedSingle&)=delete;    
    		DerivedSingle& operator =(const DerivedSingle&)= delete; 
    private:    
    		DerivedSingle()=default;
     };
    /********************************************************************************************/
    int main(int argc, char* argv[])
    {
        DerivedSingle& instance1 = DerivedSingle::get_instance();
        DerivedSingle& instance2 = DerivedSingle::get_instance();
        return 0; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    • 不需要在子类声明友元的实现方法

    • 实现要点

    精髓在于使用一个代理类 token,子类构造函数需要传递token类才能构造,把 token保护其起来, 然后子类的构造函数就可以是公有的了,这个子类只有Derived(token)的这样的构造函数,这样用户就无法自己定义一个类的实例了,起到控制其唯一性的作用。


    • 代码示例

    #pragma once
     // brief: a singleton base class offering an easy way to create singleton
    #include <iostream>
    
    template<typename T> class Singleton{ 
    public:
        static T& get_instance() noexcept(std::is_nothrow_constructible<T>::value)
        {
            static T instance{token()};
            return instance;
        }
        virtual ~Singleton() =default;
        Singleton(const Singleton&)=delete;
        Singleton& operator =(const Singleton&)=delete; 
    protected:
        struct token{}; // helper class
        Singleton() noexcept=default; 
    };
    
    
    /****************************************************************************************/ 
    // Example:
    // constructor should be public because protected `token` control theccess
    
    
    class DerivedSingle:public Singleton<DerivedSingle>{ 
    public:   
    	   DerivedSingle(token){ std::cout<<"destructor called!"<<std::endl;    }
    
       	  ~DerivedSingle(){std::cout<<"constructor called!"<<std::endl;    }    
           DerivedSingle(const DerivedSingle&)=delete;    
           DerivedSingle& operator=(const DerivedSingle&)= delete; 
    };
    /****************************************************************************************/ 
    int main(int argc, char* argv[])
    {
        DerivedSingle& instance1 = DerivedSingle::get_instance();
        DerivedSingle& instance2 = DerivedSingle::get_instance();
        return 0; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    总结

    当你需要系统中只有唯一一个实例存在的类的全局变量的时候才使用单例.
    使用时,越小越好,越简单越好,保证线程安全,内存不泄露.

  • 相关阅读:
    【点云学习PCL 】一
    学生个人网页设计作品 学生个人网页模板 简单个人主页成品 个人网页制作 HTML学生个人网站作业设计
    JUC快速学习笔记
    jdk11生成jre
    2001. 可互换矩形的组数-快速排序
    一波三折的react + less
    Kubeadm部署k8s集群 && kuboard
    Semantic Kernel Java SDK,为Java应用程序提供AI功能集成
    Java并发编程—java中守护线程和用户线程
    [极客大挑战 2019]Upload--BUUCTF
  • 原文地址:https://blog.csdn.net/qq_21438461/article/details/125393801