1. 单例模式(Singleton Pattern)
(1)定义:保证一个类仅有一个实例,同时提供能对该实例加以访问的全局访问方法。
(2)解决思路:
①在类中,要构造一个实例,就必须调用类的构造函数。如此,为了防止在外部调用类的构造函数而创建实例,需要将构造函数的访问权限设为protected或private;
②最后,需要提供全局访问点,就需要在类中定义一个static函数,返回在类内部唯一构造的实例。
2. 单例模式的实现
(1)懒汉式:
①其特点是延迟加载,典型的以“时间换空间”做法。即只会在全局访问方法首次被调用时才被创建。
②问题:new出来的实例不能自动释放,可能存在内存泄漏问题。其解决方法有2种。
A.方法1:在单例类内部定义专有的嵌套类,并在单例类定义一个私有的专门用于释放的静态成员,当程序结束析构全局变量时,会自动调用并释放这个单例。
B.方法2:在单例类提供一个专门用于释放的静态成员函数,并在需要时手动释放。
③在实际项目中,单例一般是个全局变量,它的生命期随软件运行结束而自然终止。也就没有内存泄漏,但是如果单例中用到了一个文件锁、文件句柄或数据库连接,这些并不会随程序的关闭而立即关闭,必须在程序关闭前进行手动释放。
//懒汉式1
//创建型模式:单例模式
#include <stdio.h>
//懒汉式1:缺点,须手工释放
class CSingleton
{
public:
//提供全局访问的访问点
static CSingleton* getInstance()
{
if(m_pInstance == NULL)
{
m_pInstance = new CSingleton();
}
return m_pInstance;
}
//清除:须手工调用
static void clearInstance()
{
if(m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
}
}
private:
//将构造函数设为私有属性
CSingleton(){};
//把复制构造函数和=操作符也设为私有,防止被复制
CSingleton(const CSingleton&){}
CSingleton& operator=(const CSingleton&){}
static CSingleton* m_pInstance;
};
CSingleton* CSingleton::m_pInstance = NULL;
int main()
{
CSingleton* s1 = CSingleton::getInstance();
CSingleton* s2 = CSingleton::getInstance();
printf("s1 = %p\n", s1);
printf("s2 = %p\n", s2);
CSingleton::destroyInstance();
return 0;
}
//懒汉式2
//创建型模式:单例模式
#include <stdio.h>
//懒汉式2:可自动回收垃圾
class CSingleton
{
public:
//提供全局访问的访问点
static CSingleton* getInstance()
{
if(m_pInstance == NULL)
{
m_pInstance = new CSingleton();
}
return m_pInstance;
}
private:
//将构造函数设为私有属性
CSingleton(){};
static CSingleton* m_pInstance;
//内部类,用于垃圾回收
class GC
{
public:
~GC()
{
//在这里销毁资源,比如数据库连接,句柄等。
if(m_pInstance != NULL)
{
delete m_pInstance;
m_pInstance = NULL;
printf("test: ~GC\n");
}
}
};
static GC gc;
};
//静态成员变量的初始化
CSingleton* CSingleton::m_pInstance = NULL;
//全局静态变量,会被自动销毁,从而实现对单例的垃圾回收
CSingleton::GC CSingleton::gc;
int main()
{
CSingleton* s1 = CSingleton::getInstance();
CSingleton* s2 = CSingleton::getInstance();
printf("s1 = %p\n", s1);
printf("s2 = %p\n", s2);
return 0;
}
(2)饿汉式:其特点是一开始就加载了,典型的“空间换时间”作法。因为一开始就创建了实例,所以每次用时直接返回就好了
//创建型模式:单例模式
#include <stdio.h>
//饿汉式:多线程安全
class CSingleton
{
public:
//提供全局访问的访问点
static CSingleton* getInstance()
{
static CSingleton instance;
return &instance;
}
private:
//将构造函数设为私有属性
CSingleton(){};
};
int main()
{
CSingleton* s1 = CSingleton::getInstance();
CSingleton* s2 = CSingleton::getInstance();
printf("s1 = %p\n", s1);
printf("s2 = %p\n", s2);
return 0;
}
(3)双重检查
//创建型模式:单例模式
#include <stdio.h>
#include <windows.h>
class Lock
{
private:
HANDLE m_hMutex;
public:
Lock(){m_hMutex = CreateMutex(NULL, FALSE, NULL);}
~Lock(){CloseHandle(m_hMutex);}
void lock(){WaitForSingleObject(m_hMutex, INFINITE);}
void unlock(){ReleaseMutex(m_hMutex);}
};
//多线程下的单例模式:处理懒汉式,因为饿汉式是线程安全的
class CSingleton
{
public:
//提供全局访问的访问点
static CSingleton* getInstance()
{
//双检查
if(m_pInstance == NULL) //第1次检查
{
m_lock.lock();
if(m_pInstance == NULL) //第2次检查
{
m_pInstance = new CSingleton;
}
m_lock.unlock();
}
return m_pInstance;
}
private:
//将构造函数设为私有属性
CSingleton(){};
static CSingleton* m_pInstance;
static Lock m_lock;
};
CSingleton* CSingleton::m_pInstance = NULL;
Lock CSingleton::m_lock;
//线程函数,用来测试的
DWORD WINAPI ThreadProc(PVOID pvParam)
{
CSingleton* s = CSingleton::getInstance();
printf("thread:%d, address = %p\n",(int)pvParam, s);
Sleep(1000);
return 0;
}
int main()
{
const int iCount = 64;
HANDLE hThread[iCount];
for(int i = 0; i< iCount; i++)
{
hThread[i] = CreateThread(NULL,0,ThreadProc,(LPVOID)i,0,NULL);
}
//注意:WaitForMultipleObjects最多能等待64个内核对象
WaitForMultipleObjects(iCount, hThread, TRUE, INFINITE);
for(int i = 0; i< iCount; i++)
CloseHandle(hThread[i]);
return 0;
}
3. 单例模式的应用
(1)单例模式的优点
①由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
②由于单例模式只生成一个实例,所以减少了系统性能开销,当一个对象的产生需要较多资源时,可以启用一个单例对象然后长驻内存又节约内存空间。
③单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
④单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
(2)单例模式的缺点
①单例模式一般没有接口,扩展很困难。因其“自动实例化”,而抽象类和接口是不能被实例化的。所以不能增加接口
②单例模式对测试不利。在并行的开发环境中,如果单例模式没有完成,是不能进行测试的,没有接口也就不能虚拟一个对象。
(3)使用的注意事项
①首先在高并发的情况下,要注意单例模式的线程同步问题。
②单例类一般不会主动要求被复制的,因此复制构造函数和赋值构造函数一般也设为私有。
4. 单例模式的使用场景
(1)多线程之间共享一个资源或者操作同一个对象
(2)在整个程序空间中使用全局变量,共享资源
(3)在创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。
(4)大规模系统中,为了性能的考虑,需要节省对象的创建时间
(5)要求生成唯一序列号的环境或用单例做为一个计数器。
5. 单例模式的扩展
(1)可以在单例类,定义产生实例的数量和列表,来控制创建的实例数量,这叫多例模式
(2)多例是单例的一种扩展,采用有上限的多例模式,可以在设计时决定内存中有多少个实例,以修正单例可能存在的性能问题,提高系统的响应速度。