设计模式简介:

设计模式描述了对象如何进行通信才能不牵涉相互的数据模型和方法。

保持这种独立性一直是一个好的面向对象程序设计的目标。 

Gang of Four的“Design Patterns: Elements of Resualbel Software”书将设计模式
归纳为三大类型,共23种。

创建型模式 :  通常和对象的创建有关,涉及到对象实例化的方式。(共5种模式)

行为型模式: 通常和对象间通信有关。(共11种模式)

结构型模式: 描述的是如何组合类和对象以获得更大的结构。(共7种模式)

            类模式描述的是如何使用继承提供更有用的程序接口。

            而对象模式描述的是如何通过使用对象组合或将对象包含在其他对象里,
            将对象组合成更大的一个结构。

单例模式的适用范围:

1、单例模式可以保证:在一个应用程序中,一个类有且只有一个实例,
 并提供一个访问它的全局访问点。

2、在程序设计过程中,有很多情况需要确保一个类只有一个实例。
 例如:
          windows系统中只能有一个窗口管理器

          某个程序中只能有一个日志输出系统

          一个GUI类库中,有且只有一个ImageManager

          还有其他无数种情况

教科书中的标准实现:

C++单例模式真的简单吗?_第1张图片

教课书中的标准实现的优缺点:

1、优点:该实现是一个"懒汉"单例模式,意味着只有在第一次调用GetInstance(),静  态方法的时候才进行内存分配。如果整个程序不调用该静态方法,则不会分配内存。相对应的是"饿汉"单例模式。

2、缺点:  1) "懒汉"模式虽然有优点,但是每次调用GetInstance()静态方法时,
              必须判断NULL == m_instance,使程序相对开销增大。

           2) 由于使用指针动态内存分配,我们必须在程序结束时,
              手动的调用ReleaseInstance()静态方法,进行内存的释放。

           3) 教科书标准实现最大的缺点是线程不安全。
              根据该模式的定义,整个应用程序中,不管是单线程,还是多线程,
              都只能有且只有该类的一个实例。而在多线程中会导致多个实例的产生,                 
              从而导致运行代码不正确以及内存的泄露。

教课书中的标准实现的线程不安全性演示:

C++单例模式真的简单吗?_第2张图片

C++单例模式真的简单吗?_第3张图片

C++单例模式真的简单吗?_第4张图片


1、我们创建3个辅助线程,外加main主线程,一共有4个线程。

2、我们在每个辅助线程里面调用GetInstance()静态方法,由于每个线程
  回调函数速度非常快,导致每个线程在判断NULL==m_instance时,
  都返回true,从而导致每个线程回调函数都会创建一个CSingleton1对
  象并返回指向该对象的指针。

3、我们根本没办法进行CSingleton1的内存释放,因为在多线程中,
   我们根本不知道是创建了1个、2个或3个CSingleton1的实例

Meyers Singleton Pattern实现:

C++单例模式真的简单吗?_第5张图片

Meyers Singleton Pattern的优缺点 :

1、优点:
           1)  该实现是一个"懒汉"单例模式,意味着只有在第一次调用GetInstance()时才会
                实例化。

           2)  不需要每次调用GetInstance()静态方法时,必须判断NULL==m_instance,效
                 率相对高一些。

           3)  使用对象而不是指针分配内存,因此自动回调用析构函数,不会导致内存泄露。

           4)  在多线程下的确能够保证有且只有一个实例产生。


2、缺点:

          在某些编译器中,在多线程情况下,并不是真正意义上的线程安全的实现

Meyers Singleton Pattern缺点演示:

我们修改一下前面线程函数

C++单例模式真的简单吗?_第6张图片


C++单例模式真的简单吗?_第7张图片

Meyers Singleton Pattern线程不安全性的原因:

    这是因为C++中构造函数并不是线程安全的。

    C++中的构造函数简单来说分两步:

    第一步:内存分配

    第二步:初始化成员变量

    由于多线程的关系,可能当我们在分配内存好了以后,还没来得急初始化成员变量,就
    进行线程切换,另外一个线程拿到所有权后,由于内存已经分配了,但是变量初始化还
    没进行,因此打印成员变量的相关值会发生不一致现象。

    结论:Meyers方式虽然能确保在多线程中产生唯一的实例,但是不能确保成员变量的值是否正确.

需求描述(实现一个线程安全且无内存泄漏的C++单例模式):

 1) 是一个"懒汉"单例模式,按需内存分配。

 2) 基于模板实现,具有很强的通用性。

 3) 自动内存析构,不存在内存泄露问题(使用std::tr1::shared_ptr)。

 4) 在多线程情况下,是线程安全的。

 5) 尽可能的高效。(线程安全必定涉及到线程同步,线程同步分为内核级别和用户级别的
     同步对象,用户级别效率远高于内核级别的同步对象,而用户级别效率最高的是  
     InterlockedXXXX系列API)。

 6) 这个实际上也是一个Double-Checked Locking实现的单例模式。是传统的Double-
     Checked-Locking变异版本。

线程安全的单例模式实现(基础版)

C++单例模式真的简单吗?_第8张图片

wKioL1icBFnR3Mz3AAADEPAZJY8265.png-wh_50

C++单例模式真的简单吗?_第9张图片


线程安全的单例模式(基础版)的测试

C++单例模式真的简单吗?_第10张图片

C++单例模式真的简单吗?_第11张图片


线程安全的单例模式(基础版)存在的不足

C++单例模式真的简单吗?_第12张图片

C++单例模式真的简单吗?_第13张图片


单例的一个原则就是禁止构造函数和析构函数为public,防止外部实例化,仅允许调用GetInstance()等静态方法进行初始化。

由于使用模板技术,如果我们不将基类和子类的构造和析构函数设置为public级别,模板实例化导致编译器报错。

线程安全的单例模式(基础版)的修正

1、修正构造函数:

 1)  将基类CSingletonPtr的构造函数为protected访问级别。

C++单例模式真的简单吗?_第14张图片

2)  每一个继承自CSingletonPtr的子类也将构造函数声明为protected访问级别,并在继
      承类中声明友元类。

C++单例模式真的简单吗?_第15张图片

3) 在上述代码设定以后,我们会发现对于构造函数,通过子类授权给基类的方式,我们
     能够很顺利的通过编译,代码正确的运行。

这样我们解决了防止第三方调用Manager的构造函数,Manager类的构造函数只允许在
    GetInstance()静态方法中被调用。

2、修正析构函数:
我们会发现,对于析构函数,它依旧是public访问级别的,为什么不让析构函数也声明为
    protected级别呢?

因为由于我们使用了std::tr1::shared_ptr(与boost::shared_ptr基本一致),Manager类的析构委托给了shared_ptr ,而Manager授权给的是其基类。所以如果我们将析构函数设置为protected级别,编译器会报错。

那么我们第一个反应就是我们继续在Manager中授权shared_ptr。但是没成功。可能是由于shared_ptr实现的机制导致不能成功。

难道我们真的没有办法将析构函数修正为受保护级别吗?

山穷水尽疑无路,柳暗花明又一村!

强大的shared_ptr删除器的出现解决了我们的问题!

1)  在基类CSingletonPtr中声明并实现一个访问级别为private的嵌套类Deleter,代表一个删除器。
重载函数调用操作符,该删除器类实际是一个仿函数对象。

C++单例模式真的简单吗?_第16张图片


C++单例模式真的简单吗?_第17张图片


2) 在GetInstance()静态函数中增加删除器设置代码,见图红色部分。
    一旦我们设置好删除器,那么在shared_ptr析构时不会直接调用delete而是调用删除器。
   这样我们 就将子类T的析构函数隐藏起来,不被外部调用。

3)  每一个继承自CSingletonPtr的子类也将构造函数声明为protected访问级别,但不需
      要声明授权友元类。

通过上述步骤,我们将基类和子类的构造函数都声明为受保护级别,以防止外部调用。这样整个单例子类的生命周期都由shared_ptr控制。

3、修正GetInstance()静态方法:
基础版的GetInstance()静态方法返回的是tr1::shared_ptr结构,这样导致每次调用
     GetInstance()都会使shared_ptr的引用计数加1并调用shared_ptr的拷贝构造函数。在
     调用完成GetInstance()->Print方法后,又将临时产生的shared_ptr对象引用计数减1,
     这样对效率有非常大的影响。
我们要避免这种情况,那么我们要做的是修改代码,直接在GetInstance()中返回T的引用。

C++单例模式真的简单吗?_第18张图片

更进一步,我们使用编译器提供的本质函数。


wKiom1icBiKyXk76AABLxU7FTKM741.png-wh_50

C++单例模式真的简单吗?_第19张图片

线程安全的单例模式(修正版)的优缺点

1、优点:该实现是一个"懒汉"单例模式,意味着只有在第一次调用GetInstance()
            静态方法的时候才进行内存分配。

             通过模板和继承方式,获得了足够通用的能力。

             在创建单例实例的时候,具有线程安全性。

             通过智能指针方式,防止内存泄露。

             具有相对的高效性。

2、 缺点:肯定没有单线程版本的效率高。

            每个子类必须要授权基类,我们可以写一个宏减少输入:

            #define DECLARE_SINGLETON_CLASS(type)   \
                          friend class CSingletonPtr  ;

饿汉类型单例模式实现(终极版)

饿汉模式意味着在主线程(main函数代表主线程)之前就对类进行内存分配和初始化。实现代码如下:


C++单例模式真的简单吗?_第20张图片

C++单例模式真的简单吗?_第21张图片

饿汉类型单例模式测试



C++单例模式真的简单吗?_第22张图片

C++单例模式真的简单吗?_第23张图片

C++单例模式真的简单吗?_第24张图片

单例模式四种实现总结

从编译器以及是否线程安全方面考虑:

    1、如果你使用vc6编译器,请放弃设计模式。

    2、如果你整个程序是单线程的,那么标准模式或Meyers单例模式是你最佳选择。

    3、如果你使用符合C++0X标准的编译器的话,由于C++0X标准规定:要求编译器保证内
       部静态变量的线程安全性。
       目前只有VS2015支持内部静态变量的线程安全性,因此Meyers单例模式是你最佳选择。

    4、如果你使用VC6以后,vc2010以下版本的编译器的话,并且需要线程安全,则使用实现的Double-Checked-Locking版本的单件模式。

从单例模式实现的角度考虑:

1、总是避免第三方调用拷贝构造函数以及赋值操作符

2、总是避免第三方调用构造函数

3、尽量避免第三方调用析构函数

4、总是需要一个静态方法用于全局访问

本篇文档写于2010年,当时还是以vs2008为主,因此并没有符合c++0x标准。现如今都vs2015了,c++11标准都普及了,因此Meyers单例模式是你最佳选择。
不过关于上面的shared_ptr方面的应用,还是很有价值的。shared_ptr已成为目前c++内存操作的主流技术。