C++设计模式1(单例模式)
概论
设计模式描述了对象如何进行通信才能不牵涉相互的数据模型和方法。保持这种独立性一直是一个好的面向对象程序设计的目标。Gang of Four的“Design Patterns: Elements of Resualbel Software”书将设计模式 归纳为三大类型,共23种。
创建型模式: 通常和对象的创建有关,涉及到对象实例化的方式。(共5种模式)
行为型模式: 通常和对象间通信有关。(共11种模式)
结构型模式: 描述的是如何组合类和对象以获得更大的结构。(共7种模式)
类模式描述的是如何使用继承提供更有用的程序接口;
对象模式描述的是如何通过使用对象组合或将对象包含在其他对象里,将对象组合成更大的一个结构。
单例模式
1)单例模式可以保证:在一个应用程序中,一个类有且只有一个实例,并提供一个访问它的全局访问点。
2)在程序设计过程中,有很多情况需要确保一个类只有一个实例。
例如: windows系统中只能有一个窗口管理器;
某个程序中只能有一个日志输出系统;
一个GUI类库中,有且只有一个ImageManager等等
实例1
#ifndef __CSingleton1_H #define __CSingleton1_H #include <Windows.h> #include <process.h> class CSingleton { public: //全局访问点 static CSingleton* GetInstance() { if (NULL == m_instance) { m_instance = new CSingleton(); } return m_instance; } //内存释放函数 static void ReleaseInstance() { if (NULL != m_instance) { delete m_instance; m_instance = NULL; } } void Print() { printf("print out CSingleton\n"); } private: //假设该类有很多成员变量,需要1秒钟初始化 //使用Sleep(1000)进行模拟 CSingleton() { printf("CSingleton begin constructor\n"); ::Sleep(2000); printf("CSingleton end constructor\n"); } virtual ~CSingleton() { printf("CSingleton destruct\n"); } private: //防止拷贝构造以及赋值操作 CSingleton(const CSingleton&){} CSingleton& operator=(const CSingleton&){} private: static CSingleton* m_instance; }; CSingleton* CSingleton::m_instance = NULL; #endif
优点:
该实现是一个"懒汉"单例模式,意味着只有在第一次调用GetInstance()静态方法的时候才进行内存分配。如果整个程序不调用该静态方法,则不会分配内存。相对应的是"饿汉"单例模式。
缺点:
1) "懒汉"模式虽然有优点,但是每次调用GetInstance()静态方法时,必须判断NULL == m_instance,使程序相对开销增大。
2) 由于使用指针动态内存分配,我们必须在程序结束时,手动的调用ReleaseInstance()静态方法,进行内存的释放。
3) 教科书标准实现最大的缺点是线程不安全。根据该模式的定义,整个应用程序中,不管是单线程,还是多线程,都只能有且只有该类的一个实例。而在多线程中会导致多个实例的产生,从而导致运行代码不正确以及内存的泄露。
测试程序:
#include <iostream> #include "Singleton.h" unsigned int __stdcall thread(void*) { printf("current Thread ID = %d\n", ::GetCurrentThreadId()); CSingleton::GetInstance()->Print(); //不能在线程中调用ReleaseInstance()函数,会导致程序崩溃 //在多线程下无法释放CSingleton的内存 //Csingleton::ReleaseInstance(); return 0; } void TestMultThread() { //创建三个线程 for (int i=0; i<3; i++) { //调用<process.h>中线程创建函数,该函数比Win32 CreateThread更安全 //详细参考win32核心编程相关描述,如果使用多线程,尽量使用该函数 uintptr_t t = ::_beginthreadex(NULL, 0, thread, NULL, 0, NULL); ::CloseHandle((HANDLE)t); } } void main() { TestMultThread(); getchar(); }
运行结果:
1)创建3个辅助线程,外加main主线程,一共有4个线程。
2)在每个辅助线程里面调用GetInstance()静态方法,由于每个线程回调函数速度非常快,导致每个线程在判断NULL==m_instance时,都返回true,从而导致每个线程回调函数都会创建一个CSingleton1对象并返回指向该对象的指针。3)根本没办法进行CSingleton1的内存释放,因为在多线程中,我们根本不知道是创建了1个、2个或3个CSingleton1的实例
实例2:Meyers方式
#ifndef __SINGLETON2_H #define __SINGLETON2_H #include <Windows.h> #include <process.h> class CSingleton2 { public: //单例对象使用局部静态变量方式 //从而使之延迟到调用时实例化 static CSingleton2& GetInstance() { static CSingleton2 sg; return sg; } void Print() { printf("print Singleton2 count = %d\n",m_count); } private: int m_count; CSingleton2() { printf("begine construct Singleton2 count = %d\n",m_count); ::Sleep(1000); m_count=100; printf("end construct Singleton2 count = %d\n",m_count); } ~CSingleton2(){ printf("CSingleton2 Destruct\n"); } private: //防止拷贝构造以及赋值操作 CSingleton2(const CSingleton2&){}; CSingleton2& operator=(const CSingleton2&){}; }; #endif
优点:
1) 该实现是一个"懒汉"单例模式,意味着只有在第一次调用GetInstance()时才会实例化。
2) 不需要每次调用GetInstance()静态方法时,必须判断NULL==m_instance,效率相对高一些。
3) 使用对象而不是指针分配内存,因此自动回调用析构函数,不会导致内存泄露。
4) 在多线程下的确能够保证有且只有一个实例产生。
缺点:
在多线程情况下,并不是真正意义上的线程安全的实现
测试程序:
#include <iostream> #include "Singleton2.h" unsigned int __stdcall thread(void*) { printf("current Thread ID = %d\n", ::GetCurrentThreadId()); CSingleton2::GetInstance().Print(); return 0; } void TestMultThread() { //我们创建三个线程 for(int i = 0; i<3; i++) { //调用<process.h>中线程创建函数,该函数比Win32 CreateThread更安全 //为什么,请参考win32核心编程中的相关描述 //如果要使用多线程,尽量使用该函数 uintptr_t t = ::_beginthreadex( NULL, 0, thread, NULL, 0, NULL ); ::CloseHandle( (HANDLE)t ); } } void main() { TestMultThread(); getchar(); }
运行结果:
原因:
C++中构造函数并不是线程安全的。简单来说分两步:
第一步:内存分配
第二步:初始化成员变量
由于多线程的关系,可能当我们在分配内存好了以后,还没来得急初始化成员变量,就进行线程切换,另外一个线程拿到所有权后,由于内存已经分配了,但是变量初始化还没进行,因此打印成员变量的相关值会发生不一致现象。
结论:Meyers方式虽然能确保在多线程中产生唯一的实例,但是不能确保成员变量的值是否正确.
实例3:线程安全的单例模式实现
需求描述:
1) 是一个"懒汉"单例模式,按需内存分配。
2) 基于模板实现,具有很强的通用性。
3) 自动内存析构,不存在内存泄露问题(使用std::tr1::shared_ptr)。
4) 在多线程情况下,是线程安全的。
5) 尽可能的高效。(线程安全必定涉及到线程同步,线程同步分为内核级别和用户级别的
同步对象,用户级别效率远高于内核级别的同步对象,而用户级别效率最高的是
InterlockedXXXX系列API)。
6) 这个实际上也是一个Double-Checked Locking实现的单例模式。是传统的Double-
Checked-Locking变异版本。
基类:
#ifndef __SINGLETONPTR_H #define __SINGLETONPTR_H template <class T> class CSingletonPtr { private: static tr1::shared_ptr<T> m_singletonPtr; private: CSingletonPtr(const CSingletonPtr&){} CSingletonPtr& operator=(const CSingletonPtr&){} /* 注意:此处必须为public, 如果改成protected将出现: error C2248: “CSingletonPtr<T>::GetInstance”: 无法访问 protected 成员(在“CSingletonPtr<T>”类中声明) */ public: CSingletonPtr() { printf("SingletonPtr Begin Construct\n"); ::Sleep(1000); printf("SingletonPtr End Construct\n"); } virtual ~CSingletonPtr() { printf("SingletonPtr Destruct\n"); } static tr1::shared_ptr<T> GetInstance() { static volatile long lock = 0; /* if(lock == 0) lock = 1; 如果这段代码是在多线程的情况下, 而lock是跨线程共享的变量(static意味着跨线程的) 那么有可能存在着这样情况: 线程1正在比较lock == 0 而线程2 正在赋值 lock = 1 我们必须要避免这种情况 那么我们可以使用InterlockedCompareExchange函数 如果一个线程调用该函数,那么该函数会锁定lock的内存地址,其他线程就不能同时访问 从而实现多线程环境下的线程互斥 */ int ret = 0; if(ret = _InterlockedCompareExchange(&lock,1,0) != 0) { /* _InterlockedCompareExchange函数要求lock必须为volatile,且类型必须为4位, 因为该函数会锁定内存中的四个字节。 InterlockedCompareExchange是把目标操作数(第1参数所指向的内存中的数)与 一个值(第3参数)比较,如果相等,则用另一个值(第2参数)与目标操作数(第 1参数所指向的内存中的数)交换; 返回值:第一个参数原始值。 lock变量为什么加前缀volatile? volatile表示在编译程序时不对该变量相关指令进行优化。因为,优化后代码中的静态变量可能 会因更利于访问而放在CPU中的寄存器中,待该函数运行完之后再写回到内存中去,这样就不适合 该函数对内存锁定。 */ while(lock != 2) ::Sleep(0); //将cpu分过来的剩余时间片返还。 return m_singletonPtr; } tr1::shared_ptr<T> temp(new T()); m_singletonPtr = temp; lock = 2; printf("内存分配并初始化成员变量完成\n"); return m_singletonPtr; } }; //注意:模板类型的静态成员初始化 template <class T> tr1::shared_ptr<T> CSingletonPtr<T>::m_singletonPtr; #endif
使用:
#include <stdio.h> #include <process.h> #include <windows.h> #include <memory> using namespace std; #include "SingletonPtr.h" class Manager : public CSingletonPtr<Manager> { public: Manager() { printf("Manager Begin Construct\n"); ::Sleep(500); m_count = 0; printf("Manager End Construct\n"); } ~Manager() { printf("Manager Destruct\n"); } public: void Print() { printf("Hi, I'm Manager m_count = %d\n",m_count++); } private: int m_count; }; unsigned int __stdcall thread(void*) { printf("current Thread ID = %d\n", ::GetCurrentThreadId()); Manager::GetInstance()->Print(); return 0; } void TestMultThread() { //我们创建三个线程 for(int i = 0; i<3; i++) { //调用<process.h>中线程创建函数,该函数比Win32 CreateThread更安全 //为什么,请参考win32核心编程中的相关描述 //如果要使用多线程,尽量使用该函数 uintptr_t t = ::_beginthreadex( NULL, 0, thread, NULL, 0, NULL ); ::CloseHandle( (HANDLE)t ); } } void main() { TestMultThread(); getchar(); }
结果: