美文网首页C++面试
单例模式的实现分析

单例模式的实现分析

作者: XDgbh | 来源:发表于2018-08-18 17:34 被阅读2次
    • 单例模式(Singleton),也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
      许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。
      比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的
      其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
      ——但是如果没有学过设计模式的人,可能不会想到要去应用单例模式,面对单例模式适用的情况,可能会优先考虑使用全局或者静态变量的方式,
      这样比较简单,也是没学过设计模式的人所能想到的最简单的方式了。如果采用全局或者静态变量的方式,会影响封装性,难以保证别的代码不会对全局变量造成影响。
      而使用单例模式,将类设计成单例,成员变量都设成私有,要修改的话只能通过调用成员函数,更安全。

    • 适用场景:
      单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
      1.需要频繁实例化然后销毁的对象。
      2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
      3.有状态的工具类对象。
      4.频繁访问数据库或文件的对象。
      以下都是单例模式的经典使用场景:
      1.资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
      2.控制资源的情况下,方便资源之间的互相通信。如线程池等。

    • 应用场景举例:

    1. 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。
      内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
    2. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
    3. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
    4. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
    5. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
    6. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
    7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,
      主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
    8. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
    9. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
    10. 单例模式常常与工厂模式结合使用,因为工厂只需要创建产品实例就可以了,在多线程的环境下也不会造成任何的冲突,因此只需要一个工厂实例就可以了。
    #include<iostream>
    #include<stdlib.h>
    #include<stdio.h>
    #include<mutex>
    using namespace std;
    
    /*
    ———————————— C语言下的单例模式 —————————————————
    最简单的就是定义一个全局变量,这个全局变量在全局唯一代表一种东西
    或者将所有的全局变量集中放在一个结构体中并且全局只有一个结构体对象globals
    */
    struct C_singleton_globals
    {
        int loop_flag;  //可以是程序中控制某个while死循环的终止条件
        char config_log_path; //可以是统一存放日志信息的位置
    };
    //全局唯一的结构体对象指针
    typedef struct C_singleton_globals*  C_singleton_globals_t;
    C_singleton_globals_t globals_t = NULL;
    //若要放到堆内存中,可以写一个初始化函数分配内存
    C_singleton_globals_t init_gobals_t()
    {
        //如果还没有被初始化过,那就分配内存
        if (NULL == globals_t)
        {
            globals_t = (struct C_singleton_globals *)malloc(sizeof(struct C_singleton_globals));
            printf("为全局变量分配内存成功 !\n");
        }
        else
        {
            printf("为全局变量分配内存失败,之前已分配过 !\n");
        }
        return globals_t;
    }
    
    /*
    ——————————————————C++实现单例模式—————————————
    1、将构造函数设计成private,使得不能随便产生新的对象
    2、将唯一的实例对象设计成private下的static成员变量,static的作用是使得
    //成员变成类拥有的,而不是某个实例对象拥有的
    3、设计一个public的getInstance()方法,先检查是否实例过,
    //若没有再分配内存实例出来一个对象
    */
    
    //1、只适合在单线程环境下使用的代码,若是多线程,
    //也可能刚好出现同时调用getInstance()函数,而导致产生多个实例
    class singleton1
    {
    private:
        //私有构造函数,只能从类的成员函数来访问,不能从外部访问
        singleton1(){   }
        //为了避免值复制时产生新的对象副本,除了将构造函数置为私有外,复制构造函数也要特别声明并置为私有。
        singleton1(const singleton1 &);
        //私有静态成员变量的声明
        static singleton1 *pInstance1;
    
    public:
        static singleton1 * getInstance()
        {
            if (NULL == pInstance1)
            {
                pInstance1 = new singleton1();
                cout << "成功实例化pSingle1" << endl;
            }
            else
            {
                cout << "实例化pSingle1失败,已存在" << endl;
            }
            return pInstance1;
        }
    };
    //类的静态成员的定义(所以还需要指定类型)和赋值(类中只是声明),放在类的外部
    //这句代码如果放到main函数中肯定会报错,要放在main外部
    singleton1 * singleton1::pInstance1 = NULL;
    
    //2、适合在多线程环境下使用的代码,增加线程互斥锁
    class singleton2
    {
    private:
        //私有构造函数,只能从类的成员函数来访问,不能从外部访问
        singleton2(){   }
        //为了避免值复制时产生新的对象副本,除了将构造函数置为私有外,
    //复制构造函数也要特别声明并置为私有。
        singleton2(const singleton2 &);
        //私有静态成员变量的声明
        static singleton2 *pInstance2;
        //独占式互斥量,一段时间内仅一个线程可以访问
        static mutex mtx;   //设为static是为了给下面的static函数访问,则要在类定义后面初始化
    
    public:
        static singleton2 * getInstance()
        {
            //增加这个判断(著名的DCL技法,即Double Check Lock双重锁定),
    //可以不需要每次获取同一实例都先加锁再解锁,浪费资源
            if (NULL == pInstance2) 
            {
                mtx.lock(); //互斥加锁
                if (NULL == pInstance2)
                {
                    pInstance2 = new singleton2();
                    cout << "成功实例化pSingle2" << endl;
                }
                mtx.unlock();   //互斥解锁
            }
            else
            {
                cout << "实例化pSingle2失败,已存在" << endl;
            }
            return pInstance2;
        }
    };
    //类的静态成员的定义(所以还需要指定类型)和赋值(类中只是声明),放在类的外部
    //这句代码如果放到main函数中肯定会报错,要放在main外部
    singleton2 * singleton2::pInstance2 = NULL;
    mutex singleton2::mtx;  //全局变量
    
    //3、懒汉模式最佳实现代码,适用于多线程,无需加锁
    class singleton3
    {
    private:
        //私有构造函数,只能从类的成员函数来访问,不能从外部访问
        singleton3(){   }
        //为了避免值复制时产生新的对象副本,除了将构造函数置为私有外,
    //复制构造函数也要特别声明并置为私有。
        singleton3(const singleton3 &);
    
    public:
        static singleton3 * getInstance()
        {
            //C++11规定,在一个线程开始local static 对象的初始化后完成初始化前,
    //其他线程执行到这个local static对象的初始化语句就会等待,
            //直到该local static 对象初始化完成。所以C++11标准下local static对象初始化在多线程条件下安全
            static singleton3 instance3;
            return &instance3;
        }
    };
    //以上都是懒汉单例模式,能拖多久就拖多久,即只有在第一次调用getInstance函数后才有实例产生。用时间换取空间—
    //——————下面是饿汉单例模式,即程序一运行就立即产生实例,不管后面啥时候要用,甚至不用。用空间换取时间————
    class singleton4
    {
    private:
        //私有构造函数
        singleton4(){ }
        //为了避免值复制时产生新的对象副本,除了将构造函数置为私有外,复制构造函数也要特别声明并置为私有。
        singleton4(const singleton4 &);
        //私有静态指针声明
        static singleton4 *pInstance4;
    
    public:
        static singleton4 * getInstance()
        {
            return pInstance4;
        }
    };
    //直接在给静态成员定义时,创建实例
    //为何这里又可以直接调用私有构造函数呢?因为这里是全局环境,不属于任何函数内部调用(如main函数)
    singleton4 * singleton4::pInstance4 = new singleton4();
    
    //测试代码 
    int main()
    {
        C_singleton_globals_t p_g1 = init_gobals_t();
        C_singleton_globals_t p_g2 = init_gobals_t();
        singleton1 *ps1_1 = singleton1::getInstance();
        singleton1 *ps1_2 = singleton1::getInstance();
        //singleton1 s1_3;  //这句相当于调用singleton1()构造函数,只是开辟的栈内存,也不能外部调用构造函数
        //singleton1 *ps1_3 = new singleton1(); //构造函数私有,不能在外部调用
        //singleton1 ps1_3(*ps1_2);     //拷贝赋值操作创建实例副本,该构造函数也设为私有,也不能外部调用
        //singleton1 ps1_4 = *ps1_2;        //同上
    
        singleton2 *ps2_1 = singleton2::getInstance();
        singleton2 *ps2_2 = singleton2::getInstance();
    
        singleton3 *ps3_1 = singleton3::getInstance();
        singleton3 *ps3_2 = singleton3::getInstance();
        cout << "最经典懒汉模式返回函数局部静态实例指针,实例地址:" << ps3_1 << endl;
        cout << "最经典懒汉模式返回函数局部静态实例指针,实例地址:" << ps3_2 << endl;
        
        singleton4 *ps4_1 = singleton4::getInstance();
        singleton4 *ps4_2 = singleton4::getInstance();
        cout << "最经典恶汉模式返回类全局静态实例指针,实例地址:" << ps4_1 << endl;
        cout << "最经典恶汉模式返回类全局静态实例指针,实例地址:" << ps4_2 << endl;
    }
    
    • 如果要设定只能创建5个实例,怎么做?比如限制某个窗口应用程序只能最多开5个窗口
      //0、只适合于懒汉模式,在需要的时候再创建实例
      //1、增加一个类的私有静态成员变量static int instanceCount和一个静态对象指针数组
      //2、在getInstance()函数中判断instanceCount>=5,如果没有的话则new创建实例,并且instanceCount++,然后返回实例指针
      //3、如果判断的实例已经超过了5个,则返回空指针或者返回静态对象指针数组中的一个对象指针

    • 如果要增加析构函数,释放类实例并关闭打开的其他资源文件怎么办?
      //1、增加私有的析构函数,并在析构函数里写上关闭资源文件的语句
      //2、增加public的deleteInstance()函数,里面只写两句:delete pInstence; pInstance = NULL; 即将唯一new的实例释放,
      //则会先自动调用析构函数关闭其他资源,然后才释放开辟的内存

    相关文章

      网友评论

        本文标题:单例模式的实现分析

        本文链接:https://www.haomeiwen.com/subject/tbbfiftx.html