zoukankan      html  css  js  c++  java
  • c++单例设计模式---17

    原创博文,转载请标明出处--周学伟  http://www.cnblogs.com/zxouxuewei/

      全局变量在项目中是能不用就不用的,它是一个定时炸弹,是一个不安全隐患,特别是在多线程程序中,会有很多的不可预测性;同时,使用全局变量,也不符合面向对象的封装原则,所以,在纯面向对象的语言Java和C#中,就没有纯粹的全局变量。那么,如何完美的解决这个日志问题,就需要引入设计模式中的单例模式。

      何为单例模式,在GOF的《设计模式:可复用面向对象软件的基础》中是这样说的:保证一个类只有一个实例,并提供一个访问它的全局访问点。首先,需要保证一个类只有一个实例;在类中,要构造一个实例,就必须调用类的构造函数,如此,为了防止在外部调用类的构造函数而构造实例,需要将构造函数的访问权限标记为protected或private;最后,需要提供要给全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。意思很明白,使用UML类图表示如下。

    单例模式的优点:

    单例模式(Singleton)会控制其实例对象的数量,从而确保访问对象的唯一性。

    1. 实例控制:单例模式防止其它对象对自己的实例化,确保所有的对象都访问一个实例。
    2. 伸缩性:因为由类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。

    单例模式的缺点:

    1. 系统开销。虽然这个系统开销看起来很小,但是每次引用这个类实例的时候都要进行实例是否存在的检查。这个问题可以通过静态实例来解决。
    2. 开发混淆。当使用一个单例模式的对象的时候(特别是定义在类库中的),开发人员必须要记住不能使用new关键字来实例化对象。因为开发者看不到在类库中的源代码,所以当他们发现不能实例化一个类的时候会很惊讶。
    3. 对象生命周期。单例模式没有提出对象的销毁。在提供内存管理的开发语言(比如,基于.NetFramework的语言)中,只有单例模式对象自己才能将对象实例销毁,因为只有它拥有对实例的引用。在各种开发语言中,比如C++,其它类可以销毁对象实例,但是这么做将导致单例类内部的指针指向不明。

    单例适用性

      1.使用Singleton模式有一个必要条件:在一个系统要求一个类只有一个实例时才应当使用单例模式。反之,如果一个类可以有几个实例共存,就不要使用单例模式。

      2.不要使用单例模式存取全局变量。这违背了单例模式的用意,最好放到对应类的静态成员中。

      3.不要将数据库连接做成单例,因为一个系统可能会与数据库有多个连接,并且在有连接池的情况下,应当尽可能及时释放连接。Singleton模式由于使用静态成员存储类实例,所以可能会造成资源无法及时释放,带来问题。

    代码实现

      实现一:

    #include <iostream>
    using namespace std;

    class Singleton
    {
    public:
        static Singleton *GetInstance()//获取单例instance
        {
            if (m_Instance == NULL )//判断是否已经创建单例对象
            {
                m_Instance = new Singleton ();
            }
            return m_Instance;
        }

        static void DestoryInstance()//单例销毁
        {
            if (m_Instance != NULL )//判断对象是否已经销毁
            {
                delete m_Instance;
                m_Instance = NULL ;
            }
        }

        // 操作方法实现
        int GetTest()
        {
            return m_Test;
        }

    private:
        Singleton(){ m_Test = 10; }
        static Singleton *m_Instance;
        int m_Test;
    };

    Singleton *Singleton ::m_Instance = NULL;



    int main(int argc , char *argv [])
    {
        Singleton *singletonObj = Singleton ::GetInstance();
        cout<<singletonObj->GetTest()<<endl;

        Singleton ::DestoryInstance();
        return 0;
    }

    运行结果:

    这是最简单,也是最普遍的实现方式,但是,这种实现方式,有很多问题,比如:没有考虑到多线程的问题,在多线程的情况下,就可能创建多个Singleton实例,以下版本是改善的版本。

      实现二:

    文件singleton.cpp

    #include <iostream> #include "boost/thread/thread.hpp" using namespace std; boost::mutex lock; //C++没有直接的Lock操作,此处使用Boost库,仅为了说明 class Singleton { public: static Singleton *GetInstance() { if (m_Instance == NULL )//双检锁机制(加入双检锁机制,防止大量线程被阻塞) { lock.lock(); //加锁防止多线程创建多个实例 if (m_Instance == NULL ) { m_Instance = new Singleton (); } lock.unlock(); } return m_Instance; } static void DestoryInstance() { if (m_Instance != NULL ) { delete m_Instance; m_Instance = NULL ; } }   //操作方法 int GetTest() { return m_Test; } private: Singleton(){ m_Test = 0; } static Singleton *m_Instance; int m_Test; }; Singleton *Singleton ::m_Instance = NULL; int main(int argc , char *argv []) { Singleton *singletonObj = Singleton ::GetInstance(); cout<<singletonObj->GetTest()<<endl; Singleton ::DestoryInstance(); return 0; }
    Makefile如下:
    
    SRC_OBJ=singleton.o 
    SRC_BIN=singleton.bin 
    
    CXX=g++
    SRC_LIB=-lboost_system -lboost_filesystem 
    
    ${SRC_BIN} : ${SRC_OBJ}
        ${CXX} -o $@ $^ ${SRC_LIB}
    
    clean:
        ${RM} ${SRC_OBJ} ${SRC_BIN} *.o

    运行如下:

    此处进行了两次m_Instance == NULL的判断,是借鉴了Java的单例模式实现时,使用的所谓的“双检锁”机制。因为进行一次加锁和解锁是需要付出对应的代价的,而进行两次判断,就可以避免多次加锁与解锁操作,同时也保证了线程安全。但是,这种实现方法在平时的项目开发中用的很好,也没有什么问题?但是,如果进行大数据的操作,加锁操作将成为一个性能的瓶颈;为此,一种新的单例模式的实现也就出现了。

     实现三:

    #include <iostream>
    using namespace std;
    
    class Singleton
    {
    public:
        static Singleton *GetInstance()
        {
            return const_cast <Singleton *>(m_Instance);
        }
    
        static void DestoryInstance()
        {
            if (m_Instance != NULL )
            {
                delete m_Instance;
                m_Instance = NULL ;
            }
        }
    
        int GetTest()
        {
            return m_Test;
        }
    
    private:
        Singleton(){ m_Test = 10; }
        static const Singleton *m_Instance;
        int m_Test;
    };
    
    //进入主函数之前,由主线程以单线程方式完成了初始化
    const Singleton *Singleton ::m_Instance = new Singleton();
    
    int main(int argc , char *argv [])
    {
        Singleton *singletonObj = Singleton ::GetInstance();
        cout<<singletonObj->GetTest()<<endl;
        Singleton ::DestoryInstance();
    }

    运行如下:

    因为静态初始化在程序开始时,也就是进入主函数之前,由主线程以单线程方式完成了初始化,所以静态初始化实例保证了线程安全性。在性能要求比较高时,就可以使用这种方式,从而避免频繁的加锁和解锁造成的资源浪费。由于上述三种实现,都要考虑到实例的销毁,关于实例的销毁,待会在分析。由此,就出现了第四种实现方式:

      实现四:

    #include <iostream>
    using namespace std;
    
    class Singleton
    {
    public:
        static Singleton *GetInstance()
        {
            //没有使用new在堆上动态申请内存,直接在数据区申请空间,无需delet函数释放内存
            static Singleton m_Instance;
            return &m_Instance;//智能指针获取
        }
    
        int GetTest()
        {
            return m_Test++;
        }
    
    private:
        Singleton(){ m_Test = 10; };
        int m_Test;
    };
    
    int main(int argc , char *argv [])
    {
        Singleton *singletonObj = Singleton ::GetInstance();
        cout<<singletonObj->GetTest()<<endl;
    
        singletonObj = Singleton ::GetInstance();
        cout<<singletonObj->GetTest()<<endl;
    }

    运行结果:

    对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷 贝,由该类型的所有对象共享访问。

    也就是说,静态数据成员是该类的所有对象所共有的。对该类的多个对象来说,静态数据成员只分配一次内存,供所有对象共 用。所以,静态数据成员的值对每个对象都是一样的,它的值可以更新; 
    这里有篇文章,可以阅读以下:http://anotherlayer.net/2012/05/04/static-initialization-and-thread-safety/

    实例销毁

      在上述的四种方法中,除了第四种没有使用new操作符实例化对象以外,其余三种都使用了;我们一般的编程观念是,new操作是需要和delete操作进行匹配的;是的,这种观念是正确的。在上述的实现中,是添加了一个DestoryInstance的static函数,这也是最简单,最普通的处理方法了;但是,很多时候,我们是很容易忘记调用DestoryInstance函数,就像你忘记了调用delete操作一样。由于怕忘记delete操作,所以就有了智能指针;在实际项目中,其实是不在乎这个实例的销毁的。因为,全局就这么一个变量,全局都要用,它的生命周期伴随着软件的生命周期,软件结束了,它也就自然而然的结束了,因为一个程序关闭之后,它会释放它占用的内存资源的,所以,也就没有所谓的内存泄漏了。但是,有以下情况,是必须需要进行实例销毁的:

    1. 在类中,有一些文件锁了,文件句柄,数据库连接等等,这些随着程序的关闭而不会立即关闭的资源,必须要在程序关闭前,进行手动释放;
    2. 具有强迫症的程序员。

    以上,就是我总结的两点。

    虽然,在代码实现部分的第四种方法能满足第二个条件,加上析构函数能满足第一个条件。如下代码可以实现

    #include <iostream>
    using namespace std;
    
    class Singleton
    {
    public:
        static Singleton *GetInstance()
        {
            return m_Instance;
        }
    
        int GetTest()
        {
            return m_Test;
        }
    
    private:
        Singleton(){ m_Test = 10; }
        static Singleton *m_Instance;
        int m_Test;
    
        
        class GC
        {
        public :
            ~GC()
            {
                // 我们可以早这里释放所有的资源
                if (m_Instance != NULL )
                {
                    cout<< "Here is the test" <<endl;
                    delete m_Instance;
                    m_Instance = NULL ;
                }
            }
        };
        static GC gc;
    };
    
    Singleton *Singleton ::m_Instance = new Singleton();
    Singleton ::GC Singleton ::gc;
    
    int main(int argc , char *argv [])
    {
        Singleton *singletonObj = Singleton ::GetInstance();
        cout<<singletonObj->GetTest()<<endl;
    
        return 0;
    }

    在程序运行结束时,系统会调用Singleton的静态成员GC的析构函数,该析构函数会进行资源的释放,而这种资源的释放方式是在程序员“不知道”的情况下进行的,而程序员不用特别的去关心,使用单例模式的代码时,不必关心资源的释放。

    那么这种实现方式的原理是什么呢?

      由于程序在结束的时候,系统会自动析构所有的全局变量,实际上,系统也会析构所有类的静态成员变量,就像这些静态变量是全局变量一样。我们知道,静态变量和全局变量在内存中,都是存储在静态存储区的,所以在析构时,是同等对待的。

  • 相关阅读:
    迭代器、生成器
    函数(函数基础、装饰器、递归、匿名函数)
    文件处理
    python对象、引用
    字符编码
    流程控制if、while、for
    编程与编程语言
    Java源码阅读(五)—— AbstractQueuedSynchronizer
    Java并发编程(二) —— volatile
    Java源码阅读(七)—— ReentrantReadWriteLock
  • 原文地址:https://www.cnblogs.com/zxouxuewei/p/6672369.html
Copyright © 2011-2022 走看看