设计模式(Design Pattern)说白了就是一套方法论,是我们的前辈们不断试错总结出来的。一般意义上的设计模式有23种,分为创建型、结构型、行为型三大类。今天先拿最简单的单例模式开刀吧。
六大原则
在正式进入设计模式的学习之前,我们有必要简单了解一下设计模式的六大原则,在这里浅浅认识一下即可,在后续各个模式的学习中,我们自然会逐步深入。
1、单一职责原则:一个类只做一件事!只有一个能引起其变化的原因,能够有效提高可维护性。
2、开闭原则:对扩展开放,对修改关闭。通过接口和抽象类实现代码的规范化,通过继承与多态实现扩展。
3、里氏替换原则:子类能够替换父类,同时不影响程序的正确性。即子类不破坏父类的行为。
4、接口隔离原则:不是非得使用用不到的接口!将臃肿的接口拆分为多个具体接口。
5、依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体。这是开闭原则的基础。
6、迪米特法则:又叫“最少知识原则”,一个对象应尽可能少地了解其他对象(别人家的事少打听)。
还有一些常常被提及的其他原则,比如合成复用原则,强调优先使用组合(拆分为具体组件)而非继承。还有什么保持简单性,不要过度设计等。
单例模式概述
单例模式是一种创建型模式,其定义是:确保一个类只有一个实例,其自行实例化向整个系统提供这个实例。
单例模式的目的就是让全局都只有一个访问点,通过资源复用避免了重复分配资源,减少开销。这个模式可以用于游戏中多个模块共享数据,从而进行状态管理等。
实现单例模式可以分为三步:
1、通常将类的构造方法定义为私有。这是为了让这个类只能在一开始实例,杜绝在其他地方实例化的可能。
2、定义私有类的静态实例。这个实例通常由静态指针来保存,因为静态成员与程序同周期,保证该实例能够被访问。
3、提供公有的访问实例的静态方法。利用静态方法无需将类实例即可调用的特性,保证实例的唯一性。
class A {
//第一步:私有化构造函数
private:
//将默认构造函数设置为私有
A();
//禁用拷贝构造,参数名省略
A(const A&);
//将析构函数设置为私有,杜绝外部delete
~A();
//禁用赋值操作符,彻底杜绝对象复制
A& operator = (const A&);
//第二步:静态指针保存实例
private:
static A* m_instance = NULL;
//第三步:获取实例方法
public:
static A* getInstance() {
if (m_instance == NULL) {
m_instance = new A();
}
return m_instance;
}
};
//类外初始化
A* A::m_instance = NULL;
为了方便为多个类做单例实现,可以考虑使用单例模式的模板实现,简单来说就是将定义静态实例的过程制成模板。让代码复用性高,更加地灵活。
template <typename T>
class Singleton {
//和普通的单例模式差不多
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
private:
static T* singleton;
public:
static T* getInstance() {
if (singleton == NULL) {
singleton = new T();
}
return singleton;
}
};
//类外初始化
template <typename T>
T* Singleton<T>::singleton = NULL;
class A {
//为了访问到该类的私有函数,用友元
friend class Singleton<A>;
private:
A():n('A'){}
A(const A&);
~A();
A& operator = (const A&);
private:
char n;
//有了模板就不需要这里声明了
/*static A* m_instance;*/
public:
//也不需要多余的静态函数了
/*static A* getInstance() {
if (m_instance == NULL) {
m_instance = new A();
}
return m_instance;
}*/
void print() {
cout << n << endl;
}
};
//A* A::m_instance = NULL;
int main() {
Singleton<A>::getInstance()->print();
return 0;
}
总的来说,单例模式可以减少资源开销,但其没有接口,不能继承,还需要考虑线程安全(C++11以后静态局部变量的初始化是线程安全的)。
饿汉式
“要诅咒,就诅咒弱小的自己好了。这个世界上所有不利条件都是因为当事人能力不足所致。”
——《东京食尸鬼》
可以看出,金木研在处于极度饥饿的情况下会很着急,甚至会失去理智。所以饿汉式单例设计就是指不管程序要不要这个实例,都急切地早早创建好。
现在来看看C++中饿汉式的几种实现方式。
首先是类外初始化静态成员的方法。这个利用了C++中静态成员初始化的特点。类的静态成员变量和类是同一级的,如果在类外初始化会在程序运行开始时就分配内存。而局部静态成员变量是在函数内部声明的静态变量,它的内存分配和初始化是在第一次调用该函数时进行的。之后再次调用该函数,这个局部静态成员变量不会再次进行初始化,而是保留上一次调用结束时的值。
//其实和上面写的一样
template <typename T>
class Singleton {
//和普通的单例模式差不多
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
private:
static T* singleton;
public:
static T* getInstance() {
return singleton;
}
};
//类外初始化
template <typename T>
T* Singleton<T>::singleton = new T();
在C++17以后,inline修饰的局部静态成员变量可以在程序一开始就完成初始化(可以通过输出语句cout << __cplusplus << endl来查看C++标准)。
template <typename T>
class Singleton {
//和普通的单例模式差不多
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
private:
inline static T* singleton = new T();
public:
static T* getInstance() {
return singleton;
}
};
总体来说,饿汉式在程序启动时创建,天然线程安全,但会在一开始就占用内存,所以适合简单的、必用的实例,比如控制游戏流程的全局管理器。
懒汉式
“谁也没有要你活得出人头地,在人前丢脸也好,摔得满身是泥也好,那些都会成为最棒的下酒菜。”
——《银魂》
懒汉式单例设计像是看破了红尘,正如同折木奉太郎这样的节能主义,避免无谓的麻烦与消耗,只有在第一次请求的时候才创建实例,你不用,我就不管你了。
现在来看看C++中饿汉式的几种实现方式。
首先正如上文所讲,我们可以利用局部声明的静态变量让其在首次访问时才初始化,即在函数内部声明,而且这种方法在C++11以后自动保证了线程安全。
template <typename T>
class Singleton {
//和普通的单例模式差不多
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
public:
static T* getInstance() {
static T* singleton;
if (singleton == NULL) {
singleton = new T();
}
return singleton;
}
};
还有一种是在C++11以前常用的双重检查锁结构,同样可以保证线程安全。
#include <mutex>
template <typename T>
class Singleton {
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
private:
static T* singleton;
static mutex mtx;
public:
static T* getInstance() {
if (singleton == NULL) {//第一次检查
lock_guard<mutex> lock(mtx);//加锁
if (singleton == NULL) {//第二次检查
singleton = new T();
}
}
return singleton;
}
};
//类外初始化
template <typename T>
T* Singleton<T>::singleton = NULL;
template <typename T>
mutex Singleton<T>::mtx;
有个方法名为call_once,是 C++11 引入的一个标准库函数,它的作用是确保给定的可调用对象(这里用了Lambda表达式)在多线程环境下仅被调用一次。
template <typename T>
class Singleton {
//和普通的单例模式差不多
private:
Singleton(){}
Singleton(const Singleton&){}
~Singleton(){}
Singleton& operator = (const Singleton&);
private:
static T* singleton;
static once_flag initFlag;
public:
static T* getInstance() {
//这个方法可以保证只实例一次
call_once(initFlag, []() {
singleton = new T();
});
return singleton;
}
};
//类外初始化
template <typename T>
T* Singleton<T>::singleton = NULL;
template <typename T>
once_flag Singleton<T>::initFlag;
总的来说,懒汉式的主要优点是通过延迟加载避免了不必要的开销,可以用在使用率不高、不一定会用的实例,在现代C++中局部静态变量是最佳的实现方式。
小结
实话说单例模式还是一种很实用的设计模式,他可以让游戏的各个模块都能够轻松地访问到所需要的信息,十分适合用于实现存放角色信息的等功能。
如有补充纠正欢迎留言。