单例模式
定义:确保一个类只有一个实例,并为其提供一个全局的访问入口。c++
那么什么状况下使用单例?最多见的状况就是一个类须要与一个维持自身状态的外部系统进行交互,好比说打印机。大多数状况下都是多人共用一个打印机,这意味着可能由多我的同时向这个打印机发送打印任务,这个时候管理打印机的类就必须熟悉打印机的当前状态并协调这些任务的执行。这个时候就不容许存在多个打印机的实例,由于实例没法知道其余的实例所作的操做,也就没法进行总体的管理。编程
咱们先看看最多见的单例的实现方式:服务器
class FileSystem { public: static FileSystem& instance_() { if(instance_ == nullptr) { instance_ = new FileSystem(); } return instance_; } private: FileSystem(){} static FileSystem* instance_; };
c++11保证一个局部静态变量初始化只进行一次,哪怕实在多线程的状况下也是如此,因此c++11中这样写更优雅。多线程
class FileSystem { public: static FileSystem& instance() { static FileSystem& instance_ = new FileSystem(); return instance_; } private: FileSystem(){} };
特性
从代码实现上看,单例模式由如下几个特性:并发
- 若是咱们不使用它,就不会建立实例。
- 它在运行时初始化。还有一种方法是使用静态类,但静态类有个局限就是:自动初始化。并且它是在main函数以前初始化,这也就意味了它不能使用运行时才能知道的信息,而且不能相互依赖——编译器并不能保证静态函数间初始化的顺序。
- 你能够继承单例,这可让咱们更好的控制咱们的代码,好比对于多平台的文件系统,咱们定义两个子类继承FileSystem的接口,经过一个编译指令控制文件系统类型的绑定,程序的其余代码能够与文件系统解耦(由于其余代码只是用FileSystem::instance())。
后悔使用单例模式的缘由
(1)它是个全局变量框架
根据前人的经验,全局变量时有害的,咱们应该远离全局变量。为何了?函数
- 它令代码晦涩难懂。原本在一个函数中,咱们只须要关注函数段的局部代码便可,但若是在函数中使用了全局变量,则咱们就须要追踪全部能改变全局变量状态的代码,若是这样的代码由成百上千行,你就会痛恨全局变量了。
- 全局变量促进了耦合。由于全局变量的特性,你只须要包含相应的头文件,就可使用这个变量,这就增长了代码的耦合程度。
- 它对并发并不友好,这个显而易见。
(2)它是个多此一举的方案布局
从定义上看出,单例模式实际上是解决了两个问题:第一保证一个实例,第二提供已访问入口。保证一个单例是颇有用的,但谁说咱们但愿谁都能操做它?而第二个问题,便利的访问一般是咱们使用单例的主要缘由。但这同时也会引出新的问题,好比一个日志类,一开始你们都使用这个单例的日志类时很方便,但随着项目的深刻,对于日志的需求也复杂了起来,好比要求分类写入多个日志文件,这个时候由于你是单例,因此为了支持多个实例,你就要修改每一个你调用这个类的地方,结果便利的访问也就不那么便利了。性能
(3)延迟初始化剥离了你的控制spa
延迟初始化也就是在第一次调用的时候初始化,这样也就不能保证你初始化的时机。这一般在对性能要求很是高的游戏中时不被容许的,设想一个音频单例单例,初始化须要几百毫秒,并且伴随着内存的分配,若是你的游戏进行中忽然调用这个单例,则会进行初始化操做,这件带来不可接受的游戏掉帧和卡顿。并且也不利于内存布局的控制。
在游戏中一般使用这样的方式来实现单例模式:
class FileSystem { public: static FileSystem& instance() { return instance_; } private: FileSystem(){} static FileSystem instance_; };
咱们应该使用单例吗?
(1)首先看看你需不需类
在游戏中,我看见了太多的“manager”类了,它们的初衷时为了管理其它对象,虽然有时确实有用,但我更多的时看到它被滥用。好比下面的一个例子:
class Bullet { public: int getX() const {return x_;} int getY() const {return y_;} void setX(int x) {x_=x;} void setY(int y) {y_=y;} private: int x_; int y_; }; class BulletManager { public: Bullet* create(int x,int y) { Bullet* bullet = new Bullet(); bullet->setX(x); bullet->setY(y); return bullet; } bool isOnScreen(Bullet& bullet) { return bullet.getX() >=0 && bullet.getY >=0 && bullet.getX() <= SCREEN_WIDTH && bullet.getY() <= SCREEN_HEIGHT; } void move(Bullet& bullet) { bullet.setX(bullet.getX() + 5); } };
这个例子有点极端,但现实中不少manager类简化后就是这样的一个逻辑。咱们经过一个单例来管理Bullet,感受上好像合理,但仔细分析后,发现这个manager根本就没有存在的必要,设计出这样一个类的人应该对OOP不太熟悉。首先咱们分析这三个方法:
- create建立一个Bullet,若是咱们想要更好的管理Bullet的建立,那咱们应该使用工厂模式,它会使咱们的代码可维护性更高,或者直接在Bullet中提供一个静态函数来建立一个新的对象,从设计上来讲更显得合理;
- isOnScreen判断是否在屏幕中,这个方法既能够放在业务层代码中也能够放入Bullet中(由于能够理解我为这是bullet的一个状态),把它放在这个Manager类中显得不三不四;
- move是移动bullet,这个就好设计了,move原本就是bullet的行为,因此若是没有特别的需求,move应该放入Bullet类中。
因此,修改后,咱们只须要一个Bullet类:
class Bullet { public: Bullet(int x,int y):x_(x),y_(y) { } bool isOnScreen() { return x_ >=0 && y_ >=0 && x_ <= SCREEN_WIDTH && y_ <= SCREEN_HEIGHT; } void move() { x_ += 5; } };
这样修改后,类的设计显得更合理,更天然。咱们彻底不须要一个额外的manager单例来帮助咱们管理,因此,在咱们设计单例时,首先就要分析咱们是否真的须要这个单例。
(2)将类限制为单一实例
咱们使用单例模式,不少时候只是要限制该类只有一个实例,但这并不意味着咱们要提供一个全局访问,咱们可能只是想在某一部分代码中访问这个实例,这个时候若是使用单例模式提供一个全局的访问接口,将会削弱总体的框架。咱们能够有几种方式避免这种状况的出现。好比:
class FileSystem { public: FileSystem() { assert(!instantiated); instantiated = true; } ~FileSystem() { instantiated = false; } private: static bool instantiated; }; bool FileSystem::instantiated = false;
经过一个断言,保证FileSystem只有一个实例。
(3)为实例提供便捷的访问方式
使用单例模式的另外一个需求就是便利的访问,它能让咱们随时随地的获取这个惟一的实例。但这与咱们通用的编程准则不符,咱们一般是在保证功能的状况下尽可能限制变量使用的一个范围,这样咱们就只须要记住它的地方机会少不少(想一想全局变量带来的问题)。那在不适用单例模式的时候,咱们还有什么其它的途径访问一个对象了?一般咱们会有这么几种方式:
- 做为参数传递进去。这个是最简单,一般也是最好的方法。但有时咱们会碰到这样的状况,即这个对象与函数的内容没什么必然的联系,好比咱们执行一个渲染函数时要记录日志,若是把日志对象加入到函数的参数列表中,将会很是的奇怪,对于这种状况,咱们须要一些其它的办法。
- 在基类中获取它。这须要设计一个良好的继承体系,既然全部的子类都要访问这个对象,咱们能够把这个对象放到父类中让全部的子类都能访问到它。
- 使用其它全局对象访问它。现实中咱们不太可能把全部的全局变量都移除,好比在大部分的游戏代码中咱们都会定义一个表明整个游戏状态的Game或者World对象,咱们能够把全局对象放入这些已有的全局变量中来减小它们的数量。
- 使用服务定位其来访问。这是一种专门设计一个类来给对象作全局访问的,将会在服务器定位模式一节讲解。
结语
因此,咱们应该在何时使用单例了?老师说,单例并无你想象的那样重要,若是你要确保类只被实例化一次,能够简单的使用一个静态类,若是还不知足要求,可使用一个静态的标识符在运行时检查是否只有一个实例被建立。不过使用与否仍是要视你本身需求来定,但必定要防止单例模式的滥用,这不会给你带来任何的好处。