单例模式可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。C++一般的方法是将构造函数、拷贝构造函数以及赋值操作符函数声明为private级别,从而阻止用户实例化一个类。那么,如何才能获得该类的对象呢?这时,需要类提供一个public&static的方法,通过该方法获得这个类唯一的一个实例化对象。
单例模式有许多种实现方法,在C++中,甚至可以直接用一个全局变量做到这一点,但这样的代码显的很不优雅。 使用全局对象能够保证方便地访问实例,但是不能保证只定义一个对象。
第一个版本
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton& p)
{
}
friend CSingleton& theSingleton();
};
CSingleton& theSingleton()
{
static CSingleton s;
return s;
}
特点
- CSingleton的构造函数为私有的,可以防止额外对象的产生;
- 全局函数theSingleton被声明为此类的一个友元,导致theSingleton不受私有构造函数的限制;
- theSingleton内含一个静态的CSingleton对象,意思是只有一个CSingleton对象会被产生出来
缺点
- 将theSingleton函数放在全局作用域中了,而我们知道全局变量是一种拙劣的技巧
第二个版本
namespace SingletonStuff
{
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
CSingleton(const CSingleton& p)
{
}
friend CSingleton& theSingleton();
};
CSingleton& theSingleton()
{
static CSingleton s;
return s;
}
}
两个细节
- 唯一对象是函数中的静态对象而不是类中的静态对象
- static对象和inline的互动,theSingleton函数除了第一次被调用以外,其余情况下只有一行返回代码,为什么不让其成为inline来提高效率呢?因为对于非成员函数而言,inline意味着这个函数具有内部连接,对于有内部连接的函数,可能会在程序中被复制,也就是说程序的目标代码可能会对带有内部连接的函数复制一份以上的代码,而复制行为也包括函数内的静态对象。
当我们设计单例模式的时候,这个唯一对象是函数中的静态对象而不是类中的静态对象有两点好处:
- 如果是类拥有一个静态对象,那就是说即使该对象从未被用到过,他也会构造和析构。相反,如果是函数拥有一个静态对象就是说此对象在函数第一次被调用时才产生。如果该函数从未被调用,这个对象就绝不会产生。
- 函数中静态对象的初始化时机是确定的,即该函数第一次被调用并执行到定义处的时候,而一个类静态对象则不一定在什么时候初始化,C++对于同一编译单元内的静态对象的初始化顺序有一些保证,但对于不同编译单元内的静态对象的初始化顺序没有任何说明
第三个版本
《设计模式》一书中给出了一种很不错的实现,定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并提供一个公有的静态方法获取该实例。
单例模式通过类本身来管理其唯一实例,唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例是由单例类在静态成员函数中完成创建实例的操作。习惯上把这个成员函数叫做getInstance(),它的返回值是唯一实例的指针。
class CSingleton
{
private:
CSingleton() //构造函数是私有的
{
}
static CSingleton *m_pInstance;
public:
static CSingleton * getInstance()
{
if(m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
用户访问唯一实例的方法只有getInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。getInstance()使用懒惰初始化,也就是说唯一的实例对象是当这个函数首次被访问时被创建的。
特点
- 它有一个指向唯一实例的静态指针m_pInstance,并且是私有的;
- 它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;
- 它的构造函数是私有的,这样就不能从别处创建该类的实例。
缺点
- m_pInstance指向的空间什么时候释放呢?(该实例的析构函数什么时候执行?)
- 多线程情况下是否线程安全?
版本四
如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么版本三无法实现这个要求。我们需要一种方法来正常的删除该实例。
方法一
可以在程序结束时调用getInstance(),并对返回的指针调用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用getInstance函数。
方法二
一个妥善的方法是让这个类在合适的时候把自己删除。我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):
class CSingleton
{
private:
CSingleton()
{
}
static CSingleton *m_pInstance;
class CGarbo //它的唯一工作就是在析构函数中删除CSingleton的实例
{
public:
~CGarbo()
{
if(CSingleton::m_pInstance)
delete CSingleton::m_pInstance;
}
};
static CGarbo Garbo; //定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数
public:
static CSingleton * GetInstance()
{
if(m_pInstance == NULL) //判断是否第一次调用
m_pInstance = new CSingleton();
return m_pInstance;
}
};
类CGarbo被定义为CSingleton的私有内嵌类,以防该类被在其他地方滥用。程序运行结束时,系统会调用CSingleton的静态成员Garbo的析构函数,该析构函数会删除单例的唯一实例。
特点
- 在单例类内部定义专有的嵌套类;
- 在单例类内定义私有的专门用于释放的静态成员;
- 利用程序在结束时析构全局变量的特性,选择最终的释放时机;
- 使用单例的代码不需要任何操作,不必关心对象的释放。
缺点
- 没有考虑到线程安全问题
版本五
考虑到线程安全、异常安全,可以做以下扩展
class Lock
{
private:
CCriticalSection m_cs;
public:
Lock(CCriticalSection cs) : m_cs(cs)
{
m_cs.Lock();
}
~Lock()
{
m_cs.Unlock();
}
};
class Uncopyable
{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator = (const Uncopyable&);
};
class Singleton : public Uncopyable
{
private:
Singleton();
public:
static Singleton *Instantialize();
static Singleton *pInstance;
static CCriticalSection cs;
};
Singleton* Singleton::pInstance = 0;
Singleton* Singleton::Instantialize()
{
if(pInstance == NULL)
{ //double check
Lock lock(cs); //用lock实现线程安全,用资源管理类,实现异常安全
//使用资源管理类,在抛出异常的时候,资源管理类对象会被析构,析构总是发生的无论是因为异常抛出还是语句块结束。
if(pInstance == NULL)
{
pInstance = new Singleton();
}
}
return pInstance;
}
因为我们设计的单例类继承了Uncopyable基类,并且该基类中私有了拷贝函数(包括拷贝构造和赋值运算符重载函数),因此编译器就不会为Singleton类生成默认的拷贝函数,这样做相比于将Singleton中的拷贝函数直接私有有什么好处呢?如果直接将Singleton中的拷贝函数防止了在类外调用拷贝函数,但是友元函数、友元类以及成员函数还是可以访问的。所以我们又去掉了拷贝函数的实现,这样友元函数、友元类以及成员函数如果调用拷贝函数将会得到一个链接错误,通常将链接时错误移至编译期是一件好事儿,所以我们实现了Uncopyable类,并让Singleton继承了该类,那么任何的调用将会导致编译期错误,因为编译器无法生成默认的拷贝函数。
其实如果考虑到CPU换序的问题,我们应当将以上new Singleton()的时候先赋值给一个临时变量,然后再将临时变量赋值给pInstance