c++ Singleton 单例

1648次浏览

单例模式也称为单件模式、单子模式,可能是使用最广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出,GUI应用必须是单鼠标,MODEM的联接需要一条且只需要一条电话线,操作系统只能有一个窗口管理器,一台PC连一个键盘。

单例模式有许多种实现方法,在C++中,甚至可以直接用一个全局变量做到这一点,但这样的代码显的很不优雅。 使用全局对象能够保证方便地访问实例,但是不能保证只声明一个对象——也就是说除了一个全局实例外,仍然能创建相同类的本地实例。

《设计模式》一书中给出了一种很不错的实现,定义一个单例类,使用类的私有静态指针变量指向类的唯一实例,并用一个公有的静态方法获取该实例。
单例模式通过类本身来管理其唯一实例,这种特性提供了解决问题的方法。唯一的实例是类的一个普通对象,但设计这个类时,让它只能创建一个实例并提供对此实例的全局访问。唯一实例类Singleton在静态成员函数中隐藏创建实例的操作。习惯上把这个成员函数叫做Instance(),它的返回值是唯一实例的指针。
定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CSingleton
{
private:
	CSingleton()   //构造函数是私有的
	{
	}
	CSingleton(const CSingleton &);  
	CSingleton & operator = (const CSingleton &); 
 
	static CSingleton *m_pInstance;
public:
	static CSingleton * getInstance()
	{
		if(m_pInstance == NULL)  //判断是否第一次调用
			m_pInstance = new CSingleton();
		return m_pInstance;
	}
};

用户访问唯一实例的方法只有getInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。getInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。这是一种防弹设计——所有getInstance()之后的调用都返回相同实例的指针:
CSingleton* p1 = CSingleton :: getInstance();
CSingleton* p2 = p1-> getInstance();
CSingleton & ref = * CSingleton :: getInstance();

关于Singleton(const Singleton);和 Singleton & operate = (const Singleton&);函数,需要声明成私有的,并且只声明不实现。这样,如果如果通过 CSingleton copy = *CSingleton::getInstance()等方式来使用单例时,不管是在友元类中还是其他的,编译器都是报错。如果在c++11中,可以Singleton(const Singleton) = delete;来明确函数是删除的。

单例类CSingleton有以下特征:
它有一个指向唯一实例的静态指针m_pInstance,并且是私有的;
它有一个公有的函数,可以获取这个唯一的实例,并且在需要的时候创建该实例;
它的构造函数是私有的,这样就不能从别处创建该类的实例。

大多数时候,这样的实现都不会出现问题。有经验的读者可能会问,m_pInstance指向的空间什么时候释放呢?更严重的问题是,该实例的析构函数什么时候执行?

如果在类的析构行为中有必须的操作,比如关闭文件,释放外部资源,那么上面的代码无法实现这个要求。我们需要一种方法,正常的删除该实例。
可以在程序结束时调用getInstance(),并对返回的指针掉用delete操作。这样做可以实现功能,但不仅很丑陋,而且容易出错。因为这样的附加代码很容易被忘记,而且也很难保证在delete之后,没有代码再调用getInstance函数。

一个妥善的方法是让这个类自己知道在合适的时候把自己删除,或者说把删除自己的操作挂在操作系统中的某个合适的点上,使其在恰当的时候被自动执行。
我们知道,程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。利用这个特征,我们可以在单例类中定义一个这样的静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。如下面的代码中的CGarbo类(Garbo意为垃圾工人):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CSingleton
{
private:
	CSingleton()
	{
	}
	CSingleton(const CSingleton &) = delete;  
	CSingleton & operator = (const CSingleton &) = delete; 
 
	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的析构函数,该析构函数会删除单例的唯一实例。
使用这种方法释放单例对象有以下特征:
在单例类内部定义专有的嵌套类;
在单例类内定义私有的专门用于释放的静态成员;
利用程序在结束时析构全局变量的特性,选择最终的释放时机;
使用单例的代码不需要任何操作,不必关心对象的释放。

进一步的讨论
但是添加一个类的静态对象,总是让人不太满意,所以有人用如下方法来重新实现单例和解决它相应的问题,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 
class CSingleton
{
private:
	CSingleton()   //构造函数是私有的
	{
	}
	CSingleton(const CSingleton &) = delete;  
	CSingleton & operator = (const CSingleton &) = delete;  
public:
	static CSingleton * getInstance()
	{
		static CSingleton instance;   //局部静态变量
		return &instance;
	}
};

一般来说一个程序里会有大量的单例,如果每个都这么写肯定是很累的,那么就考虑实现一个单例模板。一般来说呢,会有好几个版本的模板实现。这里就不一一说明了,只提供我自己觉得算是比较好的一种方式,同时也包括了原子操作及非原子操作。该代码使用了c++11特性。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
 
class $atomic {};        // 原子,线程安全
class $nonatomic {};     // 非原子,非线程安全
 
template <class T, class Atomicable = $nonatomic> 
class Singleton
{
 
protected:
    Singleton() {}
    virtual ~Singleton() {}
 
public:
    static inline T* getInstance();
 
private:
    static T* __getInstance(const $nonatomic&);
    static T* __getInstance(const $atomic&);
};
 
template inline T* Singleton<T, Atomicable>::getInstance()
{
    static_assert(std::is_same<Atomicable, $nonatomic>::value
                  || std::is_same<Atomicable, $atomic>::value, "Atomicable must be nonatomic or atomic");
 
    // 参考STL,用类型来规避 if...else...
    static T* s = __getInstance(Atomicable());
    return s;
}
 
template T* Singleton<T, Atomicable>;::__getInstance(const $nonatomic&)
{
    static std::auto_ptr auto_s;
    if (auto_s.get() == nullptr) {
        auto_s.reset(new T);
    }
    return auto_s.get();
}
 
template T* Singleton<T, Atomicable>::__getInstance(const $atomic&)
{
    static std::auto_ptr auto_s;
 
    if (auto_s.get() == nullptr) {
        static std::mutex mutex;
        mutex.lock();
        if (auto_s.get() == nullptr) {
            auto_s.reset(new T);
        }
        mutex.unlock();
    }
 
    return auto_s.get();
}
 
#define SINGLETON_DECLARE(T, ...)    \
public: \
static T* getInstance() { return Singleton<T, ##__VA_ARGS__>::getInstance(); } \
private:    \
    T();    \
    T(const T&) = delete;    \
    T&amp; operator=(const T&) = delete; \
    friend class Singleton<T, ##__VA_ARGS__>;
 
 
/******** Singleton Simple *********/
/*
 // Simple.h
 // =================================
 class Simple
 {
    SINGLETON_DECLARE(Simple, [ $nonatomic | $atomic ])
 public:
 
 };
 
 // Simple.cpp
 // =================================
 Simple::Simple()
 {
 
 }
 
 **************************************/

之所以在__getInstance(const $atomic&)函数里面对auto_s.get() == nullptr 是否为空做了两次判断,因为该方法调用一次就产生了对象,pInstance == nullptr 大部分情况下都为false,如果按照原来的方法,每次获取实例都需要加锁,效率太低。而改进的方法只需要在第一次调用的时候加锁,可大大提高效率。在第二次判断是因为,可能当mutex.lock()成功后,auto_s.get()已经不为空了。虽然概率很低,但在多线程的情况下还是要必须规避的。

实现的略有点复杂,不过用起来只用一行代码就要可以了。还有一个额外的功能是,不管什么类,通过Singleton::getInstance()都将得到一个该类的单例。也就是说你甚至可以不在类的头里声明SINGLETON_DECLARE,也可以使用单例。