首页 > 编程语言 >C++之单例模式(6千字长文详解)

C++之单例模式(6千字长文详解)

时间:2023-09-15 17:03:09浏览次数:46  
标签:Singleton psins sins private static C++ 单例 长文 千字

单例模式

什么是单例模式

单例模式是设计模式的一种

设计模式: 设计模式(Design Pattern)是一套被反复使用、多数人知晓的、经过分类的、代码设计经验的总结。为什么会产生设计模式这样的东西呢?就像人类历史发展会产生兵法。最开始部落之间打 仗时都是人拼人的对砍。后来春秋战国时期,七国之间经常打仗,就发现打仗也是有套路的,后 来孙子就总结出了《孙子兵法》。孙子兵法也是类似。 使用设计模式的目的:为了代码可重用性、让代码更容易被他人理解、保证代码可靠性。 设计模式使代码编写真正工程化;设计模式是软件工程的基石脉络,如同大厦的结构一样。

单例模式: 一个类只能创建一个对象,即单例模式,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息,这种方式简化了在复杂环境下的配置管理。

单例模式一共有两种实现模式

饿汉模式

#include <iostream>
#include<map>
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
    //.....
private:
       map<string,int> _info;
};
//我们要求这个类全局只有唯一一份,而且易于访问!

我们应该怎么进行设计呢?——设计成全局变量?如果是全局变量有一个我们就可以有第二个第三个!——那么我们应该如何保证只能有一个对象呢?

  • 首先构造函数私有化!
#include <iostream>
#include<map>
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
private:
    Singleton()//首先我们就要封死构造函数!
    {}
private:
    map<string,int> _info;
};
int main()
{
    //如果不封死构造函数我们就可以随意的创建对象了
    Singleton info;
    Singleton info2;
    Singleton info3;
    return 0;
}
  • 创建一个实例化的全局对象和获取这个实例化全局对象的接口!
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
    static Singleton& GetInstance()//一般单例会提供一个静态函数接口用来获取实例化对象!
    {
        //那么我们该如何创建一个全局的实例化对象呢?
        //首先肯定不是创建全局对象因为构造函数被私有化无法在外面调用!
		//我们可以创建一个静态的成员变量,这个静态的生命周期就是全局的!
        return _sins;
        //然后返回这个对象就可以了!
    }
private:
    Singleton()
    {}
    Singleton(const Singleton& ) = delete;
    Singleton operator=(const Singleton&) = delete;
private:
    map<string,int> _info;
private:
    static Singleton _sins;//创建静态的成员变了
};
Singleton Singleton::_sins;
int main()
{
    Singleton& sins = Singleton::GetInstance();//我们就可以获取到这个对象了!
    return 0;
}
  • 禁止拷贝构造和赋值重装
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
    static Singleton& GetInstance()//一般单例会提供一个静态函数接口用来获取实例化对象!
    {
        //那么我们该如何创建一个全局的实例化对象呢?
        //首先肯定不是创建全局对象因为构造函数被私有化无法在外面调用!
		//我们可以创建一个静态的成员变量,这个静态的生命周期就是全局的!
        return _sins;
        //然后返回这个对象就可以了!
    }
private:
    Singleton()
    {}
    Singleton(const Singleton& ) = delete;
    Singleton operator=(const Singleton&) = delete;
private:
    map<string,int> _info;
private:
    static Singleton _sins;//创建静态的成员变了
};
Singleton Singleton::_sins;
int main()
{
    Singleton& sins = Singleton::GetInstance();//我们就可以获取到这个对象了!
    
    //如果我们不禁用哦我们就可以
    Singleton copy = Singleton::GetInstance();//调用了拷贝构造
    Singleton copy2 = copy;//调用了operator=
    return 0;
}
  • 如果我们想要修改这个变量一般都会提供一些其他的修改接口!而不是直接修改!
#include <iostream>
#include<map>
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
    Singleton& GetInstance()//一般单例会提供一个这个函数接口用来获取实例化1对象!
    {
        return _sins;
    }
    //提供一个插入接口
    void Insert(string name,int salary)
    {
        _info[name] = salary;
    }
    void print()//提供一个打印接口
    {
        for(auto kv:_info)
        {
            cout << kv.first << ":" << kv.second <<endl;
        }
    }
private:
    Singleton()
    {}
    Singleton(const Singleton& ) = delete;
    Singleton operator=(const Singleton&) = delete;
private:
    map<string,int> _info;
private:
    static Singleton _sins;//我们可以创建一个静态的成员函数,这个静态的生命周期就是全局的!
};
Singleton Singleton::_sins;
int main()
{
    //有两种调用方法!
    Singleton& sins = Singleton::GetInstance();
    sins.Insert("张三",1000);

    Singleton::GetInstance().Insert("李四",1000000);

    sins.print();
    return 0;
}

image-20230602165309717

==这就是饿汉模式——意思就是一开始就创建对象!在main函数之前(就像是饿汉一样,等不及了直接创建对象)==

饿汉模式的缺点

  1. 如果我们单例对象的一开始要初始化的数据太多了那么就会导致启动缓慢!

==因为在main函数之前就要进行了初始化!==

在实际项目中一个单例对象不仅数据多,还可能会连接数据库,走网络那么就会导致启动缓慢

就像是我们使用的大一些的软件,有些启动很慢就是因为开始的时候初始化的数据很多

  1. 多个单例类有初始化依赖关系,饿汉模式是无法控制!

image-20230602171721913

懒汉模式

为了解决饿汉模式的缺点所以又设计了一种新的模式——懒汉模式!

==懒汉模式不会一开始就创建对象!那么是怎么实现的呢?其实也很简单==

#include <iostream>
#include<map>
#include <string>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
    static Singleton& GetInstance()
    {
        //第一次获取单例对象的时候进行创建!
        if(_psins == nullptr)
        {
            new Singleton;
        }
        return *_psins;
    }

private:
    Singleton()
    {}
    Singleton(const Singleton& ) = delete;
    Singleton operator=(const Singleton&) = delete;

private:
    map<string,int> _info;
private:
    static Singleton* _psins;//将这个修改成指针就可以了!
};
Singleton* Singleton::_psins = nullptr;
int main()
{
    Singleton& sins = Singleton::GetInstance();//这时候,对象是在这里创建的!

    return 0;
}

懒汉就是饿了不行的时候在吃

==这样子上面的问题就得到了解决!==

  1. 对象是在main函数的之后才会进行创建!不会影响启动!
  2. 我们可以主动的控制初始化顺序!

像是刚刚的上面我们说的情况!我们只要先调用database类,再调用cache类就可解决依赖问题!

而且我们要思考一个问题——new/malloc后操作系统是否真将物理内存空间给我们了?

**答案是不会的!——例如:申请了100byte,我们申请和我们未来使用可能还有很长一段时间!——作为操作系统来讲,将内存给我们了如果我们不立即使用给我们的内存空间不就是处于闲置状态吗?对于操作系统来说没有必要做这个事情!例如:我们申请100byte,但是一分钟后才会去使用!那么这个一分钟的时间内不就是白白浪费了内存空间吗?万一操作系统内有100个线程,每一个线程都怎么干!那操作系统的可操作空间不就大大的变小了!如果不立刻给,这一分钟的时间还能给别的线程用!要用的时候再开辟给进程 **

我们使用new/malloc的时候,我们申请的物理空间根本没有在物理内存上给我们开辟,而是只是在虚拟地址空间上将堆的范围扩大!然后把起始虚拟地址返回!

未来只有我们真的对这个空间写入的时候!操作系统底层就会触发缺页中断!

缺页中断就是指一旦访问内存时,发现虚拟地址通过页表转化到物理地址没有对应的映射关系!——那么此时操作系统就会停下线程的工作!会开始执行操作系统的工作!

操作系统才会去在物理内存上空间,构建虚拟地址和物理地址之间的映射关系!

==如果申请后立刻给了,那么就势必会造成申请的速度会变慢!所以我们这就是为什么懒汉模式能够优化启动速度!==

==这也是懒汉模式的特点!——延时加载(延时开辟)==

懒汉模式的缺点

==懒汉模式有线程安全的问题!==

当多个线程一起调用同一个单例对象的时候,存在线程安全的风险!

image-20230602174850025

==解决方式很简单——加锁==

#include <iostream>
#include<map>
#include <string>
#include <mutex>
using namespace std;
//单例模式的特点:全局只有一个对象!
class Singleton
{
public:
       static Singleton& GetInstance()
       {
           //双检查加锁!
           if(_psins == nullptr) //对象new出来以后,避免每次都加锁的检查,提供性能
           {
               _mut.lock();
               //因为_mut是一个静态的成员变量所以不用担心在第一次调用的时候_mut不存在!
               if (_psins == nullptr) //保证线程安全,只new一次
               {
                   new Singleton;
               }
               _mut.unlock();
           }
           //为什么我们要在外面也加上一次判断?
           //因为我们只需要在第一次的时候进行加锁,因为只有第一次会new后面就不会了!
           //如果我们不在外面多加一次判断,那么每一次都会有加锁解锁的消耗!会导致性能的消耗!
           //有了外面的一次判断在第二次进来的时候,就不会进行加锁与解锁了!
           return *_psins;
       }
       //.....
private:
       Singleton()
       {}
       Singleton(const Singleton& ) = delete;
       Singleton operator=(const Singleton&) = delete;

private:
       map<string,int> _info;
private:
       static Singleton* _psins;
       static mutex _mut;//因为静态成员函数里面没有this指针,所以我们也只能使用静态的成员变量
};
Singleton* Singleton::_psins = nullptr;
mutex Singleton::_mut;//类外面初始化

==这样子就线程安全了!——只有一个线程可以进入锁里面!==

饿汉模式是不需要加锁的!也没有线程安全的问题,因为main函数之前是不可能有两个线程的!而饿汉模式在main函数之前对象已经初始化好了!(在一般情况下饿汉其实已经够用了!)

当然了我们上面的写法也有点问题——没有异常处理!(new一旦出现异常就会跳出)如果出现异常会导致锁无法解开

//方法一——使用try-catch
class Singleton
{
public:
    static Singleton& GetInstance()
    {
        if(_psins == nullptr)
        {
            _mut.lock();
            try
            {
                if (_psins == nullptr)
                {
                    new Singleton;
                }
            }
            catch(...)
            {
                _mut.unlock();
                throw;
            }
            _mut.unlock();
        }
        return *_psins;
    }
private:
//.....
private:
    map<string,int> _info;
private:
    static Singleton* _psins;//我们可以创建一个静态的成员函数,这个静态的生命周期就是全局的!
    static mutex _mut;
};

但是这种方法很挫,代码也不好看!

==所以我们可以选择使用RAII的思路去处理==

//思路二
class LockGuard
{
public:
    LockGuard(Lock& lk)
            :_lk(lk)
    {
        _lk.lock();
    }
    ~LockGuard()
    {
        _lk.unlock();
    }
private:
   Lock& _lk;//这个不可以是Lock _lk,因为锁是不允许拷贝的!所以可以使用&或者指针
};

class Singleton
{
public:
    static Singleton& GetInstance()//一般单例会提供一个这个函数接口用来获取实例化1对象!
    {
        if(_psins == nullptr)
        {
            LockGuard<mutex> lock(_mut);//我们直接把所给这个类只要出了这个作用域这个所自己就会自己解锁!就不用担心异常导致锁无法解开了!
            if (_psins == nullptr)
            {
                new Singleton;
            }
        }
        return *_psins;
    }

private:
    //.....
private:
    map<string,int> _info;
private:
    static Singleton* _psins;//我们可以创建一个静态的成员函数,这个静态的生命周期就是全局的!
    static mutex _mut;
};

==这个就是RAII的锁管理!==

这个LockGuard其实库里面也已经给我们实现好了!都在mutex这个头文件里面

image-20230602213926450

懒汉对象的回收

==一般单例对象可以不考虑释放!因为单例对象是一个全局的对象!在整个程序运行期间都需要使用到这个单例对象!所以我们一般都不用考虑释放!当整个程序结束后,OS会自己回收这些资源!==

==单例对象不用的时候,我们就必须手动处理,一些资源需要进行保存——所以我们可以提供一些函数==

class Singleton
{
public:
       static void DELInstance()//可以使用这个函数来进行手动处理
       {
           //保存数据到文件
           //...
           std::lock_guard<mutex> lk(_mut);
           if(_psins)
           {
               delete _psins;
               _psins = nullptr;
           }
       }
    
    private:
       //....
};

==但是为了防止我们自己忘记了所以我们可以怎么做==

class Singleton
{
public:
       //....
       static void DELInstance()
       {
           //保存数据到文件
           //...
           std::lock_guard<mutex> lk(_mut);
           if(_psins)
           {
               delete _psins;
               _psins = nullptr;
           }
       }

       class GC
       {
           public:
           ~GC()//内部类是外部类的天然友元
           {
               if(_psins)//如果没有手动释放,那么就帮忙释放
               {
                   DELInstance();
               }
           }
       };
private:
       //.....
private:
       map<string,int> _info;
private:
       static Singleton* _psins;
       static mutex _mut;
       static GC _gc;
};
Singleton* Singleton::_psins = nullptr;
mutex Singleton::_mut;
Singleton::GC Singleton::_gc;
//这种写法我们既可以手动回收,也可以程序调用的时候自动让它释放!

==我们可以在里面设计一个GC的类!写一个静态的成员变量_gc,当我们忘记的调用DELInstance的时候,一旦程序结束, _gc的生命周期结束就会自动的调用析构函数去调用DELInstance==

懒汉模式的写法(2)

class Singleton
{
public:
       static Singleton& GetInstance()
       {
           //我们只要怎么写就可以了!
           static Singleton sins;
           return  sins;
       }

       void Insert(string name,int salary)
       {
           _info[name] = salary;
       }

       void print()
       {
           for(auto kv:_info)
           {
               cout << kv.first << ":" << kv.second <<endl;
           }
       }
private:
       Singleton()
       {
           cout <<"Singleton()" << endl;
       }
       Singleton(const Singleton& ) = delete;
       Singleton operator=(const Singleton&) = delete;
private:
    map<string,int> _info;
   };
int main()
{
    Singleton& sins = Singleton::GetInstance();
       sins.Insert("张三",1000);
   
    Singleton::GetInstance().Insert("李四",1000000);
       sins.print();
       return 0;
   }
 

==懒汉就是在第一次调用的时候初始化!——因为它是一个局部的静态!所以是在main函数之后进行初始化!==

image-20230603222454315

==我们可以看到下面这个代码只初始化了一次!==

static Singleton sins;

所以这也是一个单例模式!

C++11之前,sins是不能保证局部变量初始化是线程安全的!

C++11之后,可以保证!

(10 封私信 / 80 条消息) C++11 后 创建局部静态指针是否线程安全? - 知乎 (zhihu.com)

==这种写法是懒汉,但是并不是一种通用的写法!==

标签:Singleton,psins,sins,private,static,C++,单例,长文,千字
From: https://blog.51cto.com/u_15835985/7483734

相关文章

  • C++ 进阶汇总 [持续更新-建议收藏]
    1.lambda表达式```cpp#include<functional>#include<iostream>intmain(){std::function<void(void)>fun=[](){inta=1;std::cout<<a<<std::endl;};fun();}```2.using类内函数指针重命名[相同类型不同函数,放置到std::map......
  • C++ sizeof 杂谈
    原来sizeof是一个特殊的,运算优先级很高的一种运算符?之前一直都不知道。参考博客:c++中sizeof()的用法介绍C++学习杂谈:sizeof(string)到底是多少?优先级作为一个运算符,sizeof自然也是有优先级的,它在C++中优先级为\(3\),也就是除了作用域解析运算符和诸如括号的操作符,它优......
  • 设计模式 C++
    (设计模式)(李建忠C++)23种设计模式组件协作模板方法父类中定义组件(函数)的调用流程,每个组件使用虚函数进行实现,然后子类中可以重写父类中虚函数的实现。如果我们发现一个算法的组件(函数)的调用流程都是一样的,但是步骤中的各个组件的实现可能有所差异,此时会使用模板方法。【......
  • C++中STL用法汇总
    1什么是STL?STL(StandardTemplateLibrary),即标准模板库,是一个具有工业强度的,高效的C++程序库。它被容纳于C++标准程序库(C++StandardLibrary)中,是ANSI/ISOC++标准中最新的也是极具革命性的一部分。该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。为广大C++程......
  • MySQL长文本字段的选取
    某个字段需要存储长文本类型的数据,长度可变,范围不清.varchar最多能存储多大长度呢?何种情况下用text更好?<1>.先将content字段设为varchar(255),则此字段只能最多存储255个字符数packagemainimport"fmt"funcmain(){ varstrstring fori:=1;i<=255;i++{ ......
  • Qt/C++音视频开发53-本地摄像头推流/桌面推流/文件推流/监控推流等
    一、前言编写这个推流程序,最开始设计的时候是用视频文件推流,后面陆续增加了监控摄像头推流(其实就是rtsp视频流)、网络电台和视频推流(一般是rtmp或者http开头m3u8结尾的视频流)、本地摄像头推流(本地USB摄像头或者笔记本自带摄像头等)、桌面推流(将当前运行环境的系统桌面抓拍推流)。按......
  • C++完美转发为什么必须要有std::forward?
    先看一种情况,它的输出结果是什么?#include<iostream>usingnamespacestd;voidF(constint&a){cout<<"int:"<<a<<endl;}voidF(int&&a){cout<<"int&&:"<<a<<endl......
  • C++-类和对象(3)
    今天,继续和大家分享与类和对象相关的知识,本次文章的内容主要分享拷贝构造函数相关的知识。在学习拷贝构造函数之前,我们先对构造函数和析构函数进行一个总结回顾,在接这往下。构造函数和析构函数的总结回顾不论是构造函数还析构函数,我们只需要抓它们的特性,就可以很好的使用它们了。构......
  • C++下标运算符详解
    C++规定,下标运算符[]必须以成员函数的形式进行重载。该重载函数在类中的声明格式如下:返回值类型&operator[](参数);const返回值类型&operator[](参数)const;使用第一种声明方式,[]不仅可以访问元素,还可以修改元素。使用第二种声明方式,[]只能访问而不能修改元素。在实......
  • C++ STL
    Dev-C++可在工具->编译选项->代码生成/优化->代码生成->语言标准中选择“ISOC++11”或“GNUC++11”来支持C++11的新特性(蓝Dev还不支持C++14)不声明下,区间均为左闭右开区间,typename表示一个数据类型而不是C++的关键字。Containter(容器)vectorvector<t......