基于php网站建设设计,好的设计师网站有哪些,微信服务号开发方案,网站制作 番禺掌握单例模式的实现与优化 一、引言#xff1a;如何学习设计模式#xff1f;二、前置知识#xff1a;对象的创建的销毁2.1、拷贝构造2.2、拷贝赋值构造2.3、移动构造2.4、移动赋值构造 三、单例模式的定义四、单例模式的实现与优化4.1、版本一4.2、版本二4.3、版本三4.4、版… 掌握单例模式的实现与优化 一、引言如何学习设计模式二、前置知识对象的创建的销毁2.1、拷贝构造2.2、拷贝赋值构造2.3、移动构造2.4、移动赋值构造 三、单例模式的定义四、单例模式的实现与优化4.1、版本一4.2、版本二4.3、版本三4.4、版本四4.5、版本五最安全、最精简的单例模式4.6、版本六可复用的 总结 一、引言如何学习设计模式
学习设计模式最主要要抓住一点就是怎么分析这个稳定点和变化点。自己实现一个框架或者是实现一个具体的小功能本质上分析问题的思路都是一样的首先要去把稳定点给它抽象出来然后针对这个变化点想着怎么去扩展它。所以这里还是要反复的介绍怎么分析这个稳定点和变化点具体不同的设计模式是怎么来处理这个扩展就是扩展的问题稳定点它是怎么处理的用C的语言特性是怎么去解决这些问题的沿着这个思路去学习。
前面已经介绍了设计模式当中的模板方法、观察的模式、以及策略模式这里再次强调以下学习、掌握设计模式的学习步骤。
首先需要来了解设计模式解决了什么问题。本质上是分析它的稳定点和变化点实际在做具体功能开发的时候也需要去抽象具体的稳定点以及想办法去扩展变化点这样在实际开发过程当中尽量写少量的代码去应对未来需求的变化。第二点设计模式的代码结构是什么。需要培养一个看代码、看一些框架或者看项目代码结构的时候马上能够反应出来使用了什么设计模式或者它符合什么设计原则从而可以推断出代码具体的意图。熟悉实现具体设计模式的代码结构能够帮助我们对一个代码有一个敏感度以便能够快速的进行推断和反应。第三点看这些设计模式符合了哪些设计原则。因为设计模式是由设计原则推导过来的所以可以按照这一个设计模式的产生的流程重新去思考这一个问题能够帮助我们去很好的去设计我们的代码。相信很多人在具体的工作当中都有自己不同的一些设计方式它不一定符合某一些设计模式未来大家应对的某些需求也会自己去设计一个框架所以可以思考它符合哪些设计原则。第四点如何在上面扩展代码。尤其是对于初学者或刚刚参加工作的朋友们对这个扩展代码一定要非常的清楚就是如果在这个设计模式的基础上要修改哪些代码这个是必须要掌握的。第五点按照自己的需求或者自己的项目以及自己的工作场景进行一个联系哪些需求变化可以使用设计模式在看开源框架的时候也可以去看一下它是怎么解决这一个问题的。记住几个关键设计模式的一些典型应用场景能够帮助我们快速的反应当具体需求来了知道该怎么使用某一些设计模式。 #mermaid-svg-ZyXQemKmKhlRk3R1 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .error-icon{fill:#552222;}#mermaid-svg-ZyXQemKmKhlRk3R1 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZyXQemKmKhlRk3R1 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .marker.cross{stroke:#333333;}#mermaid-svg-ZyXQemKmKhlRk3R1 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .cluster-label text{fill:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .cluster-label span{color:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .label text,#mermaid-svg-ZyXQemKmKhlRk3R1 span{fill:#333;color:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .node rect,#mermaid-svg-ZyXQemKmKhlRk3R1 .node circle,#mermaid-svg-ZyXQemKmKhlRk3R1 .node ellipse,#mermaid-svg-ZyXQemKmKhlRk3R1 .node polygon,#mermaid-svg-ZyXQemKmKhlRk3R1 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .node .label{text-align:center;}#mermaid-svg-ZyXQemKmKhlRk3R1 .node.clickable{cursor:pointer;}#mermaid-svg-ZyXQemKmKhlRk3R1 .arrowheadPath{fill:#333333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-ZyXQemKmKhlRk3R1 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-ZyXQemKmKhlRk3R1 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZyXQemKmKhlRk3R1 .cluster text{fill:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 .cluster span{color:#333;}#mermaid-svg-ZyXQemKmKhlRk3R1 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZyXQemKmKhlRk3R1 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 学习步骤 设计模式解决什么问题 稳定点 变化点 设计模式的代码结构是什么 设计模式符合哪些设计原则 如何在上面扩展代码 该设计模式有哪些典型应用场景 联系工作场景 开源框架 这个就是设计模式具体的学习步骤。讲解设计模式的思路也是按照这五个步骤来进行讲解前面有一个章节已经讲解了模板方法、观察的模式、以及策略模式接下来讲解一个非常重要的设计模式单例模式。后面还会讲解工厂模式、抽象工厂模式、责任链模式、装饰模式、组合模式等这些设计模式说开发过程中常见的设计模式。
二、前置知识对象的创建的销毁
这里是通过C语言进行分析的设计模式所以会涉及到C语言的知识点特别的多。
class T{
public:T(){coutT():thisendl;}~T(){cout~T():thisendl;}T(const T){coutT(const T) 拷贝构造:thisendl;}T operator(const T){coutT operator(const T)拷贝赋值构造: thisendl;}T(T ){coutT(T )移动构造: thisendl;}T operator(T ){coutT operator(T )移动赋值构造: thisendl;}
};T CreateT(){T temp;return temp;
}
在这里构造了一个类分别有构造函数析构函数拷贝构造函数拷贝赋值构造移动移动构造以及移动赋值构造。并且进行了一个打印跟大家来介绍一下这几种构造在什么情况下会被调用这些都是比较隐藏的相信有很多的朋友根本就没有考虑过我们去实现这些东西时哪些行为会触发这些构造。
2.1、拷贝构造
拷贝构造就是用一个类构造一个对象用另外一个类去初始化。拷贝构造的触发有三个方式 1直接用对象构造。比如用这个T1类去初始化T2类那它会触发拷贝构造。
T t1;
T t2t1;2传入参数构造。
T t1;
T t2(t1);3C 11出现的 初始化列表的构造方式。
T t1;
T t2{t1};这三种方式都会触发拷贝构造。在单例模式中是不希望这个对象能够去负责构造另外一个对象显然要禁掉这种行为。
2.2、拷贝赋值构造
两个对象之间赋值。比如说有两个对象T1和T2T1复制了T2这个时候就会进入拷贝赋值构造也就是在这里有一个操作承载的这个操作符通过引用的方式去构造它。
T t1;
T t2;
t1t2;2.3、移动构造
C11出现的有两种方式
1函数返回。通过某一个函数的一个返回值将临时对象返回但是需要注意如果是没有禁掉返回值优化-fno-elide-constructorsC编译器默认会进行一个返回值优化即它其实只会有一个构造函数以及一个析构函数这个临时对象相当于这个接收的对象这会进行一个返回值的优化只会进行一次构造和一次析构。如果禁掉返回值优化-fno-elide-constructors的话这时候编译器对临时对象有三种行为
先看这个类有没有移动构造。如果没有移动构造的话就会去看有没有拷贝构造。如果这前面两个都没有的话就报错。
T tCreateT();#mermaid-svg-S8bnP9kiyZL2J812 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .error-icon{fill:#552222;}#mermaid-svg-S8bnP9kiyZL2J812 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S8bnP9kiyZL2J812 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-S8bnP9kiyZL2J812 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S8bnP9kiyZL2J812 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S8bnP9kiyZL2J812 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S8bnP9kiyZL2J812 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S8bnP9kiyZL2J812 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S8bnP9kiyZL2J812 .marker.cross{stroke:#333333;}#mermaid-svg-S8bnP9kiyZL2J812 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S8bnP9kiyZL2J812 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .cluster-label text{fill:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .cluster-label span{color:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .label text,#mermaid-svg-S8bnP9kiyZL2J812 span{fill:#333;color:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .node rect,#mermaid-svg-S8bnP9kiyZL2J812 .node circle,#mermaid-svg-S8bnP9kiyZL2J812 .node ellipse,#mermaid-svg-S8bnP9kiyZL2J812 .node polygon,#mermaid-svg-S8bnP9kiyZL2J812 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S8bnP9kiyZL2J812 .node .label{text-align:center;}#mermaid-svg-S8bnP9kiyZL2J812 .node.clickable{cursor:pointer;}#mermaid-svg-S8bnP9kiyZL2J812 .arrowheadPath{fill:#333333;}#mermaid-svg-S8bnP9kiyZL2J812 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S8bnP9kiyZL2J812 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S8bnP9kiyZL2J812 .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-S8bnP9kiyZL2J812 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-S8bnP9kiyZL2J812 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S8bnP9kiyZL2J812 .cluster text{fill:#333;}#mermaid-svg-S8bnP9kiyZL2J812 .cluster span{color:#333;}#mermaid-svg-S8bnP9kiyZL2J812 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S8bnP9kiyZL2J812 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} yes no yes no 函数返回方式 禁掉返回值优化 不禁掉返回值优化 有没有移动构造函数 OK 有没有拷贝构造函数 error 只有一个构造函数 只有一个析构函数 就是首先优先来调用它的移动构造因为移动构造不需要重新分配内存如果里面有一些动态内动态空间的话直接去去移交第二个就会去看有没有拷贝构造如果有拷贝构造会调用拷贝构造函数如果都没有那么就会报错这个是它的行为。
2std::move()。
T t1;
T t2(std::move(t1));2.4、移动赋值构造
方式一
T t;
tT();方式二
T t1,t2;
t1std::move(t2);三、单例模式的定义
学习设计模式呢首先来看一下它的定义从定义当中分析出它的具体解决了什么问题。单例模式的定义保证一个类仅有一个实例并提供一个该实例的全局访问点。 #mermaid-svg-dXCTqAs3xbg0sIdl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-dXCTqAs3xbg0sIdl .error-icon{fill:#552222;}#mermaid-svg-dXCTqAs3xbg0sIdl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dXCTqAs3xbg0sIdl .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-dXCTqAs3xbg0sIdl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dXCTqAs3xbg0sIdl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dXCTqAs3xbg0sIdl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dXCTqAs3xbg0sIdl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dXCTqAs3xbg0sIdl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dXCTqAs3xbg0sIdl .marker.cross{stroke:#333333;}#mermaid-svg-dXCTqAs3xbg0sIdl svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dXCTqAs3xbg0sIdl g.classGroup text{fill:#9370DB;fill:#131300;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-dXCTqAs3xbg0sIdl g.classGroup text .title{font-weight:bolder;}#mermaid-svg-dXCTqAs3xbg0sIdl .nodeLabel,#mermaid-svg-dXCTqAs3xbg0sIdl .edgeLabel{color:#131300;}#mermaid-svg-dXCTqAs3xbg0sIdl .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-dXCTqAs3xbg0sIdl .label text{fill:#131300;}#mermaid-svg-dXCTqAs3xbg0sIdl .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-dXCTqAs3xbg0sIdl .classTitle{font-weight:bolder;}#mermaid-svg-dXCTqAs3xbg0sIdl .node rect,#mermaid-svg-dXCTqAs3xbg0sIdl .node circle,#mermaid-svg-dXCTqAs3xbg0sIdl .node ellipse,#mermaid-svg-dXCTqAs3xbg0sIdl .node polygon,#mermaid-svg-dXCTqAs3xbg0sIdl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dXCTqAs3xbg0sIdl .divider{stroke:#9370DB;stroke:1;}#mermaid-svg-dXCTqAs3xbg0sIdl g.clickable{cursor:pointer;}#mermaid-svg-dXCTqAs3xbg0sIdl g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-dXCTqAs3xbg0sIdl g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-dXCTqAs3xbg0sIdl .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-dXCTqAs3xbg0sIdl .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-dXCTqAs3xbg0sIdl .dashed-line{stroke-dasharray:3;}#mermaid-svg-dXCTqAs3xbg0sIdl #compositionStart,#mermaid-svg-dXCTqAs3xbg0sIdl .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #compositionEnd,#mermaid-svg-dXCTqAs3xbg0sIdl .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #dependencyStart,#mermaid-svg-dXCTqAs3xbg0sIdl .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #dependencyStart,#mermaid-svg-dXCTqAs3xbg0sIdl .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #extensionStart,#mermaid-svg-dXCTqAs3xbg0sIdl .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #extensionEnd,#mermaid-svg-dXCTqAs3xbg0sIdl .extension{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #aggregationStart,#mermaid-svg-dXCTqAs3xbg0sIdl .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl #aggregationEnd,#mermaid-svg-dXCTqAs3xbg0sIdl .aggregation{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-dXCTqAs3xbg0sIdl .edgeTerminals{font-size:11px;}#mermaid-svg-dXCTqAs3xbg0sIdl :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Singleton - _instance Instance() 接下来分析一下它的稳定点和变化点解决了什么问题。解决什么问题主要来帮助分析具体设计模式解决什么问题。
稳定点类仅有一个实例并提供一个该实例的全局访问点。变化点有多个类都是单例能不能复用代码。从定义上分析只有一个稳定点即要解决一个什么问题就是解决一个类提供一个实例并且提供一个全局的好问点这样的一个问题硬要说有什么变化点的话就是可能某一个项目当中有多个类都是单例能不能复用单例模式的代码这个是牵强附会的认为要的变化点。
代码结构 1私有的构造和析构。单例模式和程序的生命周期是相同的不希望new和delete的存在应用程序退出时单例模式才会释放。所以需要把构造函数和析构函数隐藏起来让用户不能调用。 2禁掉一些构造。把所有能构造的方式都关闭。比如 拷贝构造、拷贝赋值构造、移动构造、移动拷贝构造。 3静态类成员函数。 4静态私有成员变量。
结构图
四、单例模式的实现与优化
接下来会了解一下单例模式的代码结构这里是通过C语言进行分析的设计模式所以会涉及到C语言的知识点特别的多单例模式在这里准备了六个版本来进行讲解一步一步的来看一下它分别隐藏了一些什么样的问题以及是怎么去解决它的。刚刚也跟大家说了要解决的问题就是一个实例全局访问点那么怎么来实现这个需求呢这里再反复强调一下稳定点是通过抽象出里面的稳定流程来实现这个稳定点让稳定点变得更加稳定变化点通过扩展的方式来进行扩展扩展又分为两种第一个通过继承第二个是通过组合通常通过这两种方式去扩展这些变化点。
类名对于用户而言就希望去利用它、去产生它。要实现一个只有一个实例显然要关闭这一种行为以及不希望用户去delete它也不希望别人去delete我们创建的这一个对象通常这个单例模式会跟应用程序的生命周期是一模一样应用程序退出的时候单例的对象才会得到释放。所以第一步要把这个构造函数和析构函数隐藏掉让别人不能够去调用它用户不能够去构造这个对象以及析构这个对象。
第二个需要去禁掉一些内容因为单例模式是仅有一个实例那么所有构造它的方式都要关掉它主要有四种容易忽视的构造方式
拷贝构造。拷贝赋值构造。移动构造。移动拷贝构造。
这四个是最容易被忽视的它们又能够帮助去构建对象所以呢也要把它进行限定住让其他的用户不能够去调用它。
小结
把构造和析构私有化让其他用户不能够去调用它们。禁掉一些构造方式。
下面给大家简单的来看一下几个代码。
4.1、版本一
class Singleton {
public:static Singleton * GetInstance() {if (_instance nullptr) {_instance new Singleton();}return _instance;}
private:Singleton(){}; //构造~Singleton(){};// delete 就是关闭这些行为Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton) delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton ) delete;//移动拷贝构造static Singleton * _instance;
};
Singleton* Singleton::_instance nullptr;//静态成员需要初始化
在单例模式中必须要把拷贝构造、拷贝赋值构造、移动构造、移动拷贝构造这四个禁止掉。在C11当中直接delete就可以把这些行为都给它关闭掉这些构造都不能够去构造了这个对象才能够实现一个类仅有一个实例都不能够用这一个具体的实例去构造另外一个对象。
接下来要实现一个提供单个实例的全局访问点全局的访问点也就是说在项目当中各个地方都能够访问到该实例。如果是构造一个全局对象全局对象不是很安全因为是对象的话很容易被直接去使用它直接去调用构造、拷贝构造函数现在它的构造都在private进行私有化了构造不出来所以前面代码中实现仅有一个实例的时候它就已经没法实现这种全局对象了因为所有可以构造的函数都已经放在private下面了它不能够去生成一个全局对象只能考虑从堆上去分配内存。
因此可以考虑用一个接口来访问对象提供全局访问点并且是通过静态成员函数的方式来实现访问这一个全局的访问点这个具体的值放在堆上面。因为是静态成员函数那么对应的变量_instance也必须要是静态成员变量因为如果不是静态成员变量的话在静态成员函数里面是不能够访问具体对象的变量的而只能访问静态全局变量因此必须要是一个静态的成员变量。
静态成员变量必须要进行一个初始化然后就可以通过一个if判断全家实例是否为nullptr的方式来实现一个全局的访问点从而保证只有一个实例。因为如果instance nullptr的话就会通过new Singleton如果第二次再来调用的时候它肯定不等于空指针不等于空指针就直接返回了。这里通过这种方式就实现了一个只有一个实例并且提供了一个全局的访问点。
代码结构 1私有的构造和析构。 2把所有能构造的方式都禁掉。比如 拷贝构造、拷贝赋值构造、移动构造、移动拷贝构造。 3静态类成员函数。通过他来实现我们的全局访问点。 4静态私有成员变量。通过它来帮助在堆上去分配一下内存。
从上面具体的代码实现当中思考一下存在哪些问题 首先要注意到_instance在静态全局区进行分配的它是静态全局分配的也就是说程序退出的时候这个变量它是能够释放的通过程序会自动进行释放的但是这个_instance只是一个指针它指向了一个堆上的资源但是这个堆上分配的内存它是不能够回收的。也就是析构函数是不会被调用的这肯定是有bug的要注意到析构函数是空的没必要释放这个对象的内存如果这个单例当中操作了某一块文件往文件当中写内容理论上程序推出的时候这个单例要析构调用这个析构函数去把可能要将一些数据刷到文件当中的操作继续操作完然后把这个文件的句柄close掉即把它的资源释放掉但是如果在析构函数这个地方什么都不处理的话即这里的析构函数是根本不会被调到那么文件就不会得到释放有一些没有来得及写到文件里面的内容也不会去写到文件里。
有的朋友可能想到了智能指针这里使用智能指针是可以解决的那么思考一下这个地方如果用智能指针的话应该使用什么样的智能指针智能指针有shared_ptr和unique_ptr显然使用unique_ptr可以解决这一个问题的。
4.2、版本二
接下来先不使用智能指针自己实现一份代码来解决一下上面的这个问题。
class Singleton {
public:static Singleton * GetInstance() {if (_instance nullptr) {_instance new Singleton();atexit(Destructor);// 当程序退出时调用atexit里设置的Destructor函数}return _instance;}
private:static void Destructor() {if (nullptr ! _instance) { //delete _instance;_instance nullptr;}}Singleton(){}; //构造~Singleton(){};Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton)
delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton )
delete;//移动拷贝构造static Singleton * _instance;
};
Singleton* Singleton::_instance nullptr;//静态成员需要初始化
// 还可以使⽤ 内部类智能指针来解决 此时还有线程安全问题析构函数没有被调用到的主要的原因是没有地方去把这块内存给释放掉现在增加一个接口在C语言当中有一个当程序退出的时候去回调指定方法的接口atexit(...)可以在这个接口当中去回调析构函数把相对应的内存释放掉主动调用delete。
这个就是第二个版本其他的地方都没有改变这里主要解决了一个内存泄露的问题。
4.3、版本三
在版本二中不支持多线程它是一个单线程的不支持多线程。虽然atexit(...)这个方法是一个线程安全的但是整个类来说不是线程安全的。
可以马上联想到加锁操作来看一下这个版本是怎么加锁的以及加在哪个地方。很显然哪个地方是临界资源就给哪个地方加锁。_instance new Singleton();可能会产资源竞争因为new具体对象的时候会产生资源竞争需要在这个地方进行加锁。
加锁的第一版实现代码
#include mutex
class Singleton { // 懒汉模式 lazy load
public:static Singleton * GetInstance() {// RAIIstd::lock_guardstd::mutex lock(_mutex); // 3.1 切换线程if (_instance nullptr) {_instance new Singleton();// 1. 分配内存// 2. 调用构造函数// 3. 返回指针// 多线程环境下 cpu reorder操作atexit(Destructor);}return _instance;}
private:static void Destructor() {if (nullptr ! _instance) {delete _instance;_instance nullptr;}}Singleton(){}; //构造~Singleton(){};Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton) delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton ) delete;//移动拷贝构造static Singleton * _instance;static std::mutex _mutex;
};
Singleton* Singleton::_instance nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化
按照这样的方式来进行加锁使用了RAII的思想即利用类的生命周期好来进行资源管理。从效率优先角度思考它仍然具有一些性能上的问题还可以进行一个优化。
对于这整个接口而言只有第一次调用的时候是需要加锁的调用这个类的接口的对象要获取这个类的对象的全局访问点只有第一次调用的时候才需要给进行加锁因为第一次才会涉及到写操作会有一个赋值操作分配一块内存并且调用他的构造函数其他的情况下都是读操作而读操作是没有必要加锁的所以上面的代码中会导致很多地方涉及到无用的加锁。
那么怎么解决它呢可以使用双重检测这个是编程当中的经常用到的一个技术双重检测double checking。也就是上面的代码改为两次if (_instance nullptr)而锁加在第一次和第二次检测之间。
加锁的第二个版实现代码
#include mutex
class Singleton { // 懒汉模式 lazy load
public:static Singleton * GetInstance() {if (_instance nullptr) {std::lock_guardstd::mutex lock(_mutex); // 3.2if (_instance nullptr) {_instance new Singleton();// 1. 分配内存// 2. 调用构造函数// 3. 返回指针// 多线程环境下 cpu reorder操作atexit(Destructor);}}return _instance;}
private:static void Destructor() {if (nullptr ! _instance) {delete _instance;_instance nullptr;}}Singleton(){}; //构造~Singleton(){};Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton) delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton ) delete;//移动拷贝构造static Singleton * _instance;static std::mutex _mutex;
};
Singleton* Singleton::_instance nullptr;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化
对于第一次访问这个GetInstance()接口的时候才会涉及到资源竞争也就是写操作当出现有两个及以上个线程同时进入到第一个if (_instance nullptr)里面此时只有一个线程能够持有锁持有锁的线程才会进去new这个对象另外的线程会在加锁的地方等待并且是在第二个if (_instance nullptr)的前面进行等待当获得锁的线程new完对象后并结束持有锁的生命周期第二个线程就可以持有这把锁了第二个线程持有这把锁的时候就会判断出第二个if (_instance nullptr)此时不为空那么就直接把它进行退出了。通过这种方式避免了多个线程同时进入的问题同时又增加了一个对于后面来访问这个新对象的这些读操作直接是在第一个判断就出去了因为我们这个对象已经被构造了所以就直接出去了。
4.4、版本四
上面版本三虽然解决了线程安全问题但是仍然存在一些问题。在如今的多核时代跟前面的时代已经不一样了C 98版本的语言语义是基于单线程的而C11在它的基础上进行了一些封装包括封装的这种线程操作C98线程操作都是用的标准外的一些函数比如说pthread_create和pthread_mutex等加锁都是利用的标准之外的一些库来帮助加锁C11在这上面封装了一些线程安全的操作比如std::mutex还封装了原子操作比如std::atomic以及内存栅栏。在多核时代前面的多线程操作在C 98版本都是有问题的因为在多核时代会进行一些优化包括编译器重排CPU重排等。这样子一来会带来了一个问题可能会违反顺序一致性前面写的语句必须要在后面的语句之前执行那么在多核时代编译器跟CPU都会进行一个优化它会让程序能以最快的方式来执行只要不影响后面结果的情况下可能第二条语句的执行会在第一条的前面但是这个不影响结果是因为它是考虑到对于CPU运行而言的这时就会产生一些问题
可见性问题。执行序问题。
C 11为了解决这些问题提供了一个同步原语除了同步原语还有锁。这个同步原语又分为两个部分第一个是原子变量第二个是内存栅栏啊或者叫内存屏障。下面就会利用这两个技术来帮助解决问题。
版本三虽然加了一把锁但是它没有考虑到可能会出现这种CPU的进行一个指令重排在这里CPU指令重排会出现在这个new操作也就是_instance new Singleton();这一条语句在汇编当中它是由多个指令构成的而且new是一个操作符它不是函数在具体类当中都会有一个操作符的实现这个操作符呢默认的情况下第一步先分配内存然后去调用它的构造函数最后返回指针。在多核的环境下CPU会帮助进行一个指令重排在这个语句当中可能会重排成先调用1、3然后再调用2然而本来在单核环境下调用顺序是1、2、3。这时可能就会出现内存异常问题调用1、3、2也就意味着到3时已经有内存化的地址就直接返回了但是此时它还没有去构造数据没有构构造数据当另外一个线程走到第一个if (_instance nullptr)时发现了_nstance实例不为空不为空它就返回出去返回出去它就可能要操作这个对象数据操作对象数据就会发现这个构造函数都还没有构造那里面虽然内存指针确实指向了一块区域但是这块区域没有被初始化此时去调用里面的数据的时候就可能出现异常进而导致程序奔溃。
版本三中的加锁只是从单线程语义就是C 98的时候思程序的一种方式对于现在现代语言也就是多核时代这种思考还不够还要考虑这种指令重排的问题考虑怎么用C的语言特性来解决这个问题即通过C 11提供的一些同步原语来帮助解决这个问题。
#include mutex
#include atomicclass Singleton{
public:static Singleton *GetInstance(){Singleton *tmp_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障if(tmpnullptr){std::lock_guardstd::mutex lock(_mutex);tmp_instance.load(std::memory_order_relaxed);if(tmpnullptr){tmp new Singleton;std::atomic_thread_fence(std::memory_order_release);// 释放内存屏障_instance.store(tmp,std::memory_order_relaxed);atexit(Destructor);}}return tmp;}
private:static void Destructor(){Singleton* tmp_instance.load(stdd::memory_order_relaxed);if(nullptr!tmp)delete tmp;}Singleton(){};~Singleton(){};Singleton(const Singleton ) delete;Singleton operator(const Singleton ) delete;Singleton(Singleton )delete;Singleton operator(Singleton )delete;static std::atomicSingleton* _instance;static std::metex _mutex;
};std::atomicSingleton* Singleton::_instance;//静态成员变量需要初始化
std::mutex Singleton::_mutex;//互斥锁初始化
// 编译
// g Singleton.cpp -o singleton -stdc11这里使用到一个原子变量我们把这个具体的指针对象的指针加上std::atomic类型的一个原子变量现在的_instance是一个原子变量原子变量解决了三个问题 1原子执行的问题。也就是同一时间只有一个线程执行它。 2可见性问题。原子变量提供了load可以看见其他线程最新操作的数据和和store修改数据让其他线程可见来解决。 store通常是写操作store操作目的是在线程里面操作修改的数据能够让其他线程对这个数据是可见的这里面要涉及到内核的知识这里就不讲特别复杂在这里有一级缓存、二级缓存、三级缓存只是核心的私有缓存在这里修改数据其他线程是不可见的 store的作用就是让其他线程可以看到数据的修改。load()是可以看见其他线程最新操作的数据。 3执行绪问题。使用内存模型解决memory_order_acquire、memory_order_release。C 11给了六个内存模型即六种内存序这里只给大家解释这两个内存序。memory_order_acquire通常对应的读操作它的意思是它后面的语句不能够优化到外面去即这一个语句的上面因为有CPU指令重排所以这个指令要求它不能够优化到上面去memory_order_release意思是它上面的代码不能够优化到它下面来这两个一起使用的就是它们中间的代码既不能出去不能在往上面去也不能够往下面去。
内存栅栏不是具体的原子变量它主要解决了可见性的问题跟执行序的问题。
解决了这个原子序的问题后安全性就解决了多线程环境下单例模式这个时候就彻底没有问题了。
4.5、版本五最安全、最精简的单例模式
上面版本四写的太复杂了写一个安全线程的代码太长了如果有多个类是构造函数那写代码的时候就有一点要抓狂的那么有一个更精简的方式直接使用静态成员变量不使用指针。主要是利用了c11 的 magic static 特性如果当变量在初始化的时候并发同时进⼊声明语句并发线程将会阻塞等待初始化结束。
// c11 magic static 特性如果当变量在初始化的时候并发同时进⼊声明语句并发线程将会阻塞等待初始化结束。
// c effective
class Singleton
{
public:static Singleton GetInstance() {static Singleton instance;return instance;}
private:Singleton(){}; //构造~Singleton(){};Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton) delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton ) delete;//移动拷贝构造
};
// 继承 Singleton
// g Singleton.cpp -o singleton -stdc11
/*该版本具备 版本4 所有优点
1. 利⽤静态局部变量特性延迟加载
2. 利⽤静态局部变量特性系统⾃动回收内存⾃动调⽤析构函
数
3. 静态局部变量初始化时没有 new 操作带来的cpu指令
reorder操作
4. c11 静态局部变量初始化时具备线程安全
*/版本四写了一个很长的代码才实现了一个安全的单例模式。其实可以利用C 11的特性用最简单的方式来实现一个安全的单例模式并且是一个线程安全的。主要利用了C11的magic特性由c effective这个作者提出来的。这是一种最安全的、最精简、最简单的一个单例模式直接用一个静态的全局变量构造对象因为静态的全局变量只会初始化一次并且是多线程安全的最重要的是它不会进行CPU指令重排和在生命周期时可以调用析构。
该版本具备 版本4 所有优点
利⽤静态局部变量特性延迟加载。利⽤静态局部变量特性系统⾃动回收内存⾃动调⽤析构函数。静态局部变量初始化时没有 new 操作带来的cpu指令reorder操作。c11 静态局部变量初始化时具备线程安全。
4.6、版本六可复用的
templatetypename T
class Singleton {
public:static T GetInstance() {static T instance; // 这⾥要初始化DesignPattern需要调⽤DesignPattern 构造函数同时会调⽤⽗类的构造函数。return instance;}
protected:virtual ~Singleton() {}Singleton() {} // protected修饰构造函数才能让别⼈继承
private:Singleton(const Singleton ) delete; //拷⻉构造Singleton operator(const Singleton) delete;//拷贝赋值构造Singleton(Singleton ) delete;//移动构造Singleton operator(Singleton ) delete;//移动拷贝构造
};class DesignPattern : public SingletonDesignPattern {//friend 能让SingletonT 访问到 DesignPattern构造函数friend class SingletonDesignPattern;
private:DesignPattern() {}~DesignPattern() {}
};
这个版本就是在版本的基础上添加了多态因为还有一个变化点没有解决如果项目当中有多个类都是单例能不能够去复用这个代码呢因为不想每一个单例都跑去实现这样的一个静态的变量、静态的函数还要写这么多这种把它拷贝构造、拷贝赋值、移动构造、移动拷贝构造全部把它给关闭掉太复杂了想去重复利用这一块代码这个时候只能使用模板来实现了通过模板加继承的方式去解决这一个变化点的问题因为变化点的扩展通常是通过继承的方式来扩展它并且加入多态的方式。用上面在版本五的基础上来进行迭代使用友元类可以让基类调用到子类的构造函数因为构造函数声明了private不设置友元会无法正常调用。
总结
通过六个示例描述一步步完善单例模式的设计过程需要考虑的问题。C类的构造有构造函数、拷贝构造、拷贝赋值构造、移动构造、移动赋值构造。编写单例模式代码时需要考虑其线程安全性问题。同一个对象它们是friend class的关系可以互相访问私有成员。单例模式是很常见的设计模式需要掌握。
思维导图