问题情景

在windows下面打开任务管理器,我们会发现,你只能打开一个任务管理器。如果在已打开一个任务管理器的情况下,再次打开任务管理器,系统会调出已经打开的窗口,而不是重新打开新的窗口。在系统中,之多只能存在一个任务管理器的实例。按照一般的思路,我们需要定义一个TaskManager的类(如下)

class TaskManager
{
public:
  TaskManager(){}
}

但这存在问题,只要我们调用一次初始化函数,系统就会生成一个TaskManager的实例,不能保证系统中之多只存在一个实例的要求。于是我们需要采用设计模式中的单例模式(Singleton)来实现。

单例模式

定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。[DP]定义中提到了单例模式的两个功能:仅有一个实例和全局访问点。我们通过Singleton的类图来看一下这个模式:Singleton在Singleton中,类的初始化函数被设为了private,这就意味着Client是不能通过调用类的初始化函数来生成一个实例,防止Client多次调用初始化函数生成多个实例出来。如果要访问Singleton,需要通过GetInstance方法,这是一个静态的方法,主要负责创建自己的实例,返回实例的指针给Client。

代码如下:

class Singleton
{
private:
    static Singleton* instance;
    Singleton(){}
public:
    static Singleton* GetInstance()
    {
        if (NULL == instance)
        {
            instance = new Singleton();
        }
        return instance;
  }
};
Singleton* Singleton::instance = NULL //静态数据成员是静态存储的,必须对他进行初始化

Singleton类封装了它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它。简单的说就是对唯一实例的受控访问。一般的,Singleton不会定义析构函数,由于实例分配的是全局地址,在程序运行结束,被系统收回所有分配空间时,Singleton才会被销毁,如果Singleton成员数据太多的话,可能占用了大量的内存空间而不能被释放,这需要我们注意。

Singleton与全局变量的区别

Singleton与全局变量都有全局访问的功能,但两者之间存在一些差别,全局变量在使用的过程中会存在一些问题:

  • 变量名冲突:必须小心维护变量名,每遇到一个全局变量,都必须仔细分辨该变量属于哪个模块、哪个程序。
  • 耦合度难题:使用全局变量增加了函数与模块间的耦合程度,不易维护。
  • 单个实体问题:全局变量不能防止一个类生成多个实例出来。
  • 多线程访问:并发访问时需要使用同步机制,小心保护全局变量,避免冲突。

看起来,使用Singleton可以避免上面的这些问题,但Singleton自身也有一些弊端。

Singleton的优缺点

Singleton的优点:

  • 跨平台:使用合适的中间件可以把Singleton扩展为跨多个计算机工作。
  • 适用于任何类:只要将初始化函数设为私有,并增加相应的静态函数和变量,就能把类变成Singleton。
  • 延迟性:如果Singleton从未使用,就不会创建(仅指懒汉模式)

Singleton的缺点:

  • 效率问题:由于if语句的存在,调用方法的效率收到影响。
  • 不变重用:在C++下需要定义模板才能实现Singleton的 重用。

Singleton的懒汉模式与饿汉模式

针对Singleton初始化函数的调用策略,有两种选择,分别是懒汉模式和饿汉模式。

懒汉模式

使用懒汉模式时,Singleton在程序第一次调用的时候才会初始化自己,代码同上。使用该模式时,由于if语句的存在,会影响调用的效率。而且,在多线程环境下使用时,为了保证只能初始化一个实例,需要用锁来保证线程安全性,防止同时多个线程进入if语句中。如果遇到处理大量数据时,锁会成为整个性能的瓶颈。一般懒汉模式适用于程序一部分中需要使用Singleton,且在实例化后没有大量频繁访问或线程访问的情况。

饿汉模式

使用饿汉模式时,Singleton在程序一开始就将自己实例化,之后的GetInstance方法仅返回实例的指针即可,这样就解决了上述提到的if语句影响效率的问题。代码如下:

class Singleton
{
private:
    Singleton* instance;
    Singleton(){};
public:
    static Singleton* GetInstance()
    {
        return instance;
    }
};

Singleton* Singleton::instance = new Singleton();//在此直接实例化

饿汉模式适用于Singleton在程序运行过程中一直被频繁调用,这样由于预先加载了实例,访问实例时没有if语句,效率更高。但要注意到,如果Singleton的成员比较庞大、复杂,实例化Singleton会花一些时间,且这个实例一直占用着大量内存,在使用时要注意这部分的开销。使用饿汉模式用于多线程编程的话,由于线程访问之前,实例已存在,就不需要像懒汉模式中加入锁,因此饿汉模式保证了多线程安全。饿汉模式比较适用于程序整个运行过程中都需要访问、会被频繁访问或者需要被多线程访问的情况。

可以继承的Singleton

Singleton简单易用,但考虑到代码复用的话,Singleton不能够被继承,如果每次都要重新写一个Singleton类是很麻烦的。如果需要一个可以被继承的Singleton父类,需要用到C++中的模板来实现。模板是C++支持参数化多态的工具,使用模板可以使用户为类或者函数声明一种一般模式,使得类中的某些数据成员或者成员函数的参数、返回值取得任意类型。

template<class S>
class Singleton
{
public:
    static S* GetInstance()
    {
        if (instance == null)
        {
            instance = new S();
        }
        return instance;
    };
protected:
    Singleton(){};
private:
    static S* instance;
    Singleton(const Singleton & ){};
    Singleton & operator = ( const Singleton & rhs ){};
};

定义好了这个Singleton的模板,如果每次需要使用Singleton,只需要使用这个模板就能实现,不需要我们每次都从零开始写一遍Singleton,大大提高了开发效率。当然,这也只是Singleton模板最简单的一个版本,对于功能更强大的Singleton模板,欢迎大家一起来交流。

登录发表评论 注册

反馈意见