c++的单例模式为什么不直接全部使用static,而是非要实例化一个对象?
在学习使用 C++ 实现单例模式时,了解到单例模式的目的是为了创建一个唯一的全局对象(例如使用一个单例对象访问所有的配置信息)
那为什么不能直接全部使用 static 变量和 static 函数呢?如果全部使用 static 的话,是不是也不会有多线程的问题了?而且 “类型::方法” 的访问方式比起先 getInstance 再访问难道不是更加简单清晰吗?(还是说是为了附和 “单例” 这样一个字面上的意思)
//大概这个样子
class Singleton {
public:
static void on() {Singleton::isOn = true;}
static void off() {Singleton::isOn = false;}
static bool state() {return Singleton::isOn;}
private:
static bool isOn;
};
你这个也不算是单例问题啊,C++虽然不阻止你像C一样开发程序,但也不建议啊。
一个类里都是静态对象,这太C风格。这样用类还不如直接声明为namespace XXX,里面放几个函数就行,那样还能把static bool isOn的定义隐藏在cpp文件中呢。
C++11下单例的政治正确如下
static Singleton Singleton::getInstance()
{
static Singleton s_instance;
return s_instance;
}
优点:
1.延迟加载,保证不用到就不会构造这个类,只会在第一次用到时构造(这算不算比你说的强?);
2.C++11保证这个s_instance的构造是线程安全的,多线程随意调用(注意,仅仅只保证构造是线程安全!内部实现当然是加了锁,还有判断是否已构造的代码,开销非常小。要是强迫症,你可以调用后暂存这个引用);
3.完全杜绝了静态存储对象在不同编译单元构造顺序的问题,想怎么调怎么调(只要你别故意在单例类的构造函数之间制造循环依赖,会死锁)。
举一个功能上的缺点更实在:直接全用static不方便在单元测试中进行替换。单例模式的一个常见用例是保存一个用来和外部系统通讯的stub,例如:
DatabaseConnection::GetInstance()-&>SaveData(...);
在对调用这个操作的那段业务逻辑进行单元测试的时候,通常不会真的去起一套数据库(开销很大)。取而代之的是我们会写一个FakeDatabaseConnection类或者MockDatabaseConnection类,然后做诸如这样的事情:
FakeDatabaseConnection fake_connection;
DatabaseConnection::SetFakeInstanceForTest(fake_connection);
在此之后通过GetInstance()得到的就会是fake_connection这个对象了,至于它在SaveData()的时候想干啥,就全交由测试环境来控制了。
如果不用对象的概念,就没法这么做了。我知道写太长了。所以改成就一句话好了。
“零例模式” 和 单例模式不一样。
。。。。。以下为原回答。。。。
我们从不重要到重要,一项项说。
零、是单例,还是零例?
人家模式叫“单例”,这里的“例”就是“实例”,就是“Instance”,通常就是一个对象实体。完全采用静态类,效果就是一个对象都不用创建,也能干活,这……如果我们在设计的模式叫“零例模式”,那似乎可以,但其实也不行,因为一个静态类,和普通类(非单例这类设计)在语法大可创建任意多个对象。这就是语义层面的不一致了:你告诉大家说这是单例模式,其实它可能是零例或N多例——你觉得这不重要吗?恰恰相反,最容易带来误解问题的事情就是交流过程中语义错误——你写的代码就是你说过的话。
比如,你的思路是所有成员全静态,请问构造和析构如何全静态?你至少得把构造私有化掉,从而让事情至少看似进了一步:消除了可能是N多例的情况,变成纯纯的”零例“模式。然后,事情来了,构造不能在外部调用——这是正确的做法,因为单例正是这么做——那么初始化工作谁来做,什么时机做?比如你有个成员是指针,你想在构造为它分配内存,于是好像是需要一个静态方法来确保做初始化工作,如果你为你的”零例模式“添加了”Init()“方法,那么所有使用这个类的程序员(包括你自己)都会在某年某月某个深夜心生怀疑:是不是由于种种原因,造成我在Init()之前使用这个类了?或者,是不是我多调用了一次“Init()”?。而人家单例模式不会有这个问题。我们对比一下。
- “零例”模式:
//大家好,我是零例模式作者,请大家在调用零例所有方法之前,一定要记得
//调用且仅调用一次Init();求求大家了,好吗?不然会出错的。
Zeroton::Init();
Zeroton::foo();
你自然可以说,我至少有办法让Init()支持重入!这不典型的没问题创造问题也要解决的问题的干劲吗?
- 单例模式:
Singleton::Instance().foo();
没错,借由正确的设计,我们通过语法限定了使用这个模式的人,先是不容易理解错,因为直观, 再是就算一时不理解,也被“管”得死死的,不容易干蠢事。
如果,非要突出我大c++的不同之处,并不是c++诞生之夜的雷雨交加,而是,尽管c++做设计模式也少不了“约定”。那由于c++语言自身实现某事的方法由于选择太多,而容易选择出错的c++猪对友的杀伤力又远大于其它语言的队友。所以c++做设计模式多少要更强调一点“约束”和语义清晰。用你的零例模式相比单例模式,约定多一点,约束少了点。外加语义乱了一点。
任何玩c++一定时长的人,都能明白这个语义问题还能产生很多深远的效应,这里就不费嘴了。失过恋的人才懂失恋的痛。后面要谈的几乎所有问题,基本都和这个语义混淆有亲戚关系。
为什么以要把“初始化”和“语义”扯一起讲?因为有“例”所以才有初始化的工作和初始化的时机。对于真正的单例,一切很明确也很好理解 :有例,有生死(我们暂不谈死),有初始化。有对象的产生(构造)时机。对于“零例”模式,这很难理解:明明没有例,无生无死,可还要初始化?什么时候初始化大家才不会抢或避?
处女座的人到这一点就可以得道了——
那没有失恋的人要如何避免感情上的弯路呢?我认为就是当一个处女座:处女座只要想到自己其实是在写着零例模式在模拟单例模式……她就会放弃这种名实对不上的感受了。
一、写得累
你觉得:
Singleton::foo();
似乎要比:
Singleton::Instance().foo();
写起来舒服一点。其实不然,后者完全可以在必有反复使用时,写成:
auto singleon(Singleton::Instance());
singleon.foo();
我想说的是你在定义类时,写一堆static不累吗?何况不少static成员数据都需要在class之外再定义一处。来,我问你一句:同一个类中,不同的两个静态成员数据,但是在同一个CPP文件中做类外定义,请问它们在初始化的次序,是依赖于类中的声明次序,还是类外的定义次序?是不是刚一听会有点犹豫?这又是在制造人生困惑。
所有懒人都到这一点都可以得道了。
这一点问题和第一点的亲戚关系是: 零例模式往往要有N多例(成员数据)放在类外定义来成全自己的零。
二、和OO、和泛型,全都不好配合
- 单例模板化
非常容易写一个单例的模板对不对?但请问如何模板化一个零例方式。尽管c++11写单例简单太多,但在实际大一点团队中,不知道 C++11对函数域内static数据所提供的保障的人都有人在,这些人你让写自己动手写单例,他们就会上来就定义一个mutex,然后写成C++98版本的。
或者倒过来想:在C++11之前,使用老旧不完美的方式写单例,但幸好是写成模板了,到了想长级为C++11时,只需改改这个模板就好。这就是模板起的“约束”作用。如果是“零例”呢?大家是项目组出一个规章:
1)所有初始化动作,统一由“InitOnece()”或“InitOnConstruct()”或者“InitBUTDontCallMeOneMore()”方法调用。不能图简单的叫“Init()”。
2)……
2.单例的传递
单例还需要传递????难道不是到处这样使用吗:
Singleon::Instance().XXX()?
当然不是:一个大项目,十几个库,想让单例对所有库都可见,那它们不就是在库(Module)范围内的“全局变量”了吗? 大家都知道在一个程序内应该少用全局变量,为什么要在系统模块之间扩散单例?(更何况,很多单例它就是在某个库的范围内才是单例,比如和某个数据库的连接池等)。
事实上,好的设计是:任何设计都不要轻易扩散。所以哪怕在同一个程序库内,如果某个子模块就是不需要知道:哦:这个家伙是一个单例,那你就不应该逼它知道。
试举一例。比如说:你开了一家小公司,购买了一台网络打印机。功能齐全,性能强大。估计5年内公司的发展也就只需要这一台网络打印机了。管理层为方便管理,决定 定义一个"ThePrinter"的单例。这很正确!然后,美工部门写了一个打印美图的函数 ,如果 你逼他们说:设计“PrintImage()”的时候请注意:我们公司的打印机是单例!于是就是这么一个函数 :
void PrintImage(Image const img)
{
ThePrinter::Instance().print("版权所有
");
ThePrinter::Instance().print(img);
}
有一天,你家打印机出错了,公司让你紧急使用PrintImage()方法去街上找一家打印店,必须在5分钟之内打出公司营执照图,事关1个亿的大生意。嘿嘿。我能笑吗?
不关心打印机是单例的PrintImage()是这样的:
void PrintImage(Printer printer, Image const img)
{
printer.print("版权所有
");
printer.print(img);
}
实在想用默认使用公司的打印机,就再加一样啊:
void PrintImage(Image const img)
{
PrintImage(ThePrint().Instance(), img);
}
回归主题 : 零例模式怎么传递对象?如果居然都有对象了,那它的一堆static方法的设计,是图什么?
以是偏OO的设计,然后,与模板也受干扰,对比如下:
- 单例模式:我不用被逼着知道 “这个家伙必须是单例”,我大可就认为它是一个“例”。
template &
class C
{
...
T t;
t.print(...);
};
2.零例模式:如果我不知道这是一个零例模式,那我怎么会知道要这样调用:
T::print(...);
最后是模板OO 组合。复杂的设计:我想要一个单例工厂。是这样的,我公司每年都会评奖一次年度老年男神,年度中年男神,年度老年女神,年度中年女神等。每个神都是在这一年内的单例,即一年内一类(class)神就一例(object),之前公司在这方面管理欠缺,没有使用严格的单例实现,结果造成大量伪神骗吃骗喝的混乱现象。但这些神之间都有种相似这处,有个共同接口基类,叫“年度神”,里面有一堆纯虚/pure virtual方法。我要有个
告诉我:一个静态的成员方法,可以同时是virtual的吗?
累。不写了。
构造函数调用时间不一样,static在main函数之前初始化,是不可控的,没法控制初始化顺序。
你说的对。所谓“模式”并不是强制要求,如果有必要,完全可以打破模式的。但是getInstance的写法是有好处的:1. 提供了对“唯一对象”进行初始化的时机
2. 大家都习惯这么写单例了,写出来其他人一看就明白
很多时候其实就是花括号换不换行的问题,两方可能各自看不顺眼,但其实都能用。
不过还是有一些很重要的区别的。
首先,静态成员变量的初始化顺序不可控。
比如a和b两个变量,b需要通过a创建。类的构造函数是能保证按照初始化列表顺序来的,但如果写成静态成员的话……由于C++不存在静态类的说法,也没有静态构造函数,所以并不能保证保证a在b之前被初始化。其次,静态成员的话是在编译期就确定好了它的位置,而如果是通过函数返回一个局部静态对象的话,那就是运行期才确定位置,并返回他的指针。
前者由于是编译期确定,所以可能编译器会多做一些不可告人的优化/改变。最后static方法其实就相当于namespace,不过依靠类的private能“隐藏”一些函数/成员。而后者其实是真正的“单例”---是成员的方法,调用时需要这个对象的指针。
因此大概还有个不同就是,一个可以当作用了语法糖的C函数,而另一个则是正统的C++函数。正因如此,后者能享受C++类的优点,比如virtual的多态;而前者能当作C函数导出吧(此处存疑,我瞎猜的)。
相比之下,性能开销我觉得是不需要考虑的。还是那句话,不做profile,不要过早优化。看了前几个top的答案。@王尼玛 列举的非常对,但似乎所有人都没有回答到最重要的一点。
使用singleton pattern最重要的原因是,singleton class可以继承,可以用来实现接口(interface)。你可以把一个singleton作为某个接口的实现传给一个接受这个接口的method。而static variable或者free function+static variable的写法都不能这样做。
这是stack overflow上获赞最多的答案。Difference between static class and singleton pattern?Difference between static class and singleton pattern?
static成员访问速度比较慢。定义的时候每个成员前面都要加static,比较费手指。C++11之前,static成员不能在类声明里初始化。
C++11之前,static不能保证线程安全。
还是那句话,如果你不用动态加载dll/so,其实也没有别的什么原因了。1。单例模式的getInstance是唯一一个公开的获取实例的静态方法,这个静态方法里面提供具体的类的创建实例的功能,在一定程度上防止这个类在其他地方被实例化。
2。关于线程安全
1) 单例模式的getInstance方法的线程安全是指防止在不同的线程中创建不同的实例(最后用一个全局指针或者静态成员变量指针保存创建的类实例),最后一个创建类实例的线程会把其他线程先创建的实例覆盖掉,导致不可预料的结果。
2)另外一个是单例类本身实现的线程安全(非实例创建的线程安全)。
3。关于代码的写法, 想简单点直接用#define mysingleton singleton::getInstance()。
从 stackoverflow 找到一个关于Java 的答案链接:http://stackoverflow.com/questions/519520/difference-between-static-class-and-singleton-pattern
A singleton allows access to a single created instance - that instance (or rather, a reference to that instance) can be passed as a parameter to other methods, and treated as a normal object.
A static class allows only static methods.
理论有时学起来很枯燥,所以说2个例子。
1. 继承。
拿cocos2d-x游戏引擎的FileUtils来做个说明,游戏业务功能层面我们只用调用 FileUtils类的静态 getInstance方法,FileUtils::getInstance()-&>func() 。。。 就可以调用需要的资源。
FileUtils类实现了许多通用的处理方法,它是一个单例,FileUtils.h里面声明了 static FileUtils* getInstance();但FileUtils.cpp里没有getInstance实现代码(FileUtils可以看成抽象类)。
getInstance的实现是放在不同平台的FileUtils子类,比如FileUtils-Apple,FileUtils-Android和FileUtils-win32(当然你也可以继续继承FileUtils-Apple等等来实现更多功能),这几个类都继承了FileUtils,并实现了static FileUtils* getInstance()。 用这种方式,增加一个新平台可以完全不用修改已有的代码,代码结构组织也非常清晰。 当然,如果全部静态+宏也能实现,不过代码维护性可能惨不忍睹。
github: cocos2d/cocos2d-x
2. 多核实现。
单例模式的目的是为了创建一个唯一的全局对象。
抽象点的比喻,某地有一个工厂A1,B公司是工厂的管理机构,在实现上,A1的系统里有一个单例类A1来统筹管理其生产业务流程。现在要扩大生产规模,在另外一个地方新建一家工厂A2,两家业务相似但不完全相同,现在B要同时管理A1和A2。
这里A1和A2构成了2个模块,每个模块内部独立,互不干涉,大概代码是这样,应该看得懂
A2和A1类似,这里不详细写了,A的getInstance需要传一个参数。 用这种方式,A1和A2依然是全局唯一,B公司系统在调用时依然只需要使用A::getInstance("Ai"),就可以控制任何一个子工厂的生产。同样的,如果未来再建立新模块或者删除已有模块代码,B公司系统可不做修改就能满足需求。
(如果还不理解,建议去了解下pureMVC的多核原理)
我总结出来大概就这两种。相比而言,用类比用静态来实现单例更加合理,结构和扩展性也相比较好。当然,如果你的系统非常小,就是一快餐式的代码,根本不用去考虑扩展和维护,单例随你怎么用了,根本没有什么非要用实例化来实现单例的说法。
从业务上来说既然使用类,那大多是为了构造复杂的数据模型,基本上会用到继承,而static 本质上属于全局变量,不能被继承,那如果我需要每个派生类都之有一份就实现不了了。
这叫 单态模式吧!
请搜索 meyers singleton
首先设计模式本来就是要教你OO,所以你理解为singleton就是静态全局变量的OO实现没问题。至于为什么,那是工程问题,纸上得来终觉浅。你就按你想的用,遇到问题了,比这里任何答案都能理解深刻.写代码不比做化学实验,搞不好就会爆炸。所以多试试是个好方法。
那为什么不能直接全部使用 static 变量和 static 函数呢?
让一个类里面的所有成员都是 static,那是 Java 的做法。在 C++ 里面,这种做法能搞定的场合,直接用一个 namespace 更合适。
你说的其实也没问题,那么静态函数放到哪个区?这个区是不是有限的?如果改成获得两个实例你得改多少代码?你这样实现是否降低耦合?多线程安全么?
这下明白最佳实现比较难了吧!简单地说, 你这种写法不是单例,因为可以创建多个Singleton的对象,虽然每个对象里的成员函数和成员变量都是一样的。
而单例模式的好处体现在,1. 类的对象只有一个,且不能以::形式调用成员函数,代码保持一致,避免了混淆、误用。2. 在GetInstance()函数里,可以自行管理对象分配。 不必非得全局对象。3. 可以运行时按指定次序创建, 容易和其他模块配合。另外其他的,比如继承上的不同,前面几楼也说了。每个静态成员变量要在cpp中定义一次是不是很烦
你只考虑了成员函数的问题,如果这个class还应该有一个或多个数据成员呢?你这样就没办法实现唯一实例的要求了。
推薦閱讀:
※「Bjarne 為增加 C++ 程序員身價有意增加 C++ 難度」是否屬實?
※這種寫法用意何在?
※string和char數組的區別是什麼以及map可否設置key為char數組?
※为什么ASCII被摆放成现在这个样子?——我指的是各个ASCII字符的编号顺序
※如何評價 ScyllaDB?