• 设计模式学习笔记---单例模式


    1. 单例模式的简单实现

    2. 单例模式的特点

    3. 多线程安全的单例模式

    4. 模版类的单例模式的实现

    5. 使用单例模式需要注意的问题

    1. 简单的单例模式如下:

     1 class Singleton {
     2 private:
     3     Singleton() {};
     4     ~Singleton() {};
     5 public:
     6     static Singleton* getInstance() //全局访问点接口
     7     {
     8         if (_instance == nullptr)
     9         {
    10             _instance = new Singleton;
    11         }
    12         return _instance;
    13     }
    14     static void deleteInstance()
    15     {
    16         if (_instance != nullptr)
    17         {
    18             delete _instance;
    19             _instance = nullptr;
    20         }
    21     }
    22 private:
    23     static Singleton* _instance;  // 只能有一个实例
    24 };
    25 Singleton* Singleton::_instance = nullptr;

      单例模式实现了 某类只能有一个实例,并且提供一个全局访问点。(完整来说 除了私有化构造函数,还需要私有拷贝构造函数(其实就是构造函数一种)、赋值操作符。)

      ps1:这里有个问题:在第10行,new 了一个Singleton,此时调用了Singleton的构造函数,看上去是不是“静态成员函数调用了非静态成员函数(构造函数)”? 而静态成员函数只能调用静态成员函数。这不是矛盾了么?

      首先那么为什么静态函数不能调用非静态函数?因为静态函数没有this指针,而普通成员函数(非静态)需要this指针。

      再看那个问题,在new Singleton的时候第一步 分配了一段内存,也就有了这个实例的this指针,第二步再调用构造函数,此时需要的this指针就是第一步创建的。也就是说对这个新的对象 调用他的静态和非静态成员函数(当然是要有访问权限的函数)都是可以的,因为在调用非静态成员函数时的this指针是这个新的对象实例的地址,而不是需要通过函数参数传进来this指针。只不过问题中正好是生成的是一个本类的实例。同时调用静态函数 Singleton::getInstance() 时,表明了作用域在 Singleton:: 内,那么就可以在这个函数里访问Singleton类里任何访问权限的成员。

       ps2:静态数据成员需要在类外进行定义,因为如果没有定义就是没有分配内存(类内的仅仅是声明而已),没有分配内存怎么可以访问呢。

    2. 为什么要有单例模式

        单例模式实现了 某类只能有一个实例,并且提供一个全局访问点。

      要实现只有一个全局访问点,也可以用全局变量。全局变量的缺点是,不管代码中实际用到与否,都会初始化。但是单例模式可以采用懒汉式,用到再初始化。

      要实现只有一个实例,可以用静态类来实现。

    3. 多线程安全的单例模式

    在生成实例的时候加锁:

    static Singleton* getInstance()
    {
         if (_instance == nullptr)
        {
            加锁操作
            if (_instance == nullptr)   // 想想这里为什么还要再判断一次
                _instance = new Singleton;
        }
         return _instance;
     }    
    View Code

     也可以在初始化的时候直接new, getInstance直接返回对象。不过这样就失去了他的一个优点—只有真正用到的时候才会被创建。

    Class Singleton
    {
        ....
        Singleton* getInstance()
        {
            return _instance;
        }
        ....
    }
    Singleton* Singleton::_instance = new Singleton;
    View Code

    4. 模版类的单例模式的实现

    模板类的单例模式如下:

    template<typename T>
    class Singleton
    {
    protected:                        // protected确保子类可以创建对象
        Singleton() {};
        virtual ~Singleton() {};      // 如果析构函数里面有资源的释放什么的 就必须写上 virtual
    public:
        static T* getInstance()   // 如果getInstance是protected就是只有继承体系中才能获得使用,不是全局访问。
        {
            if (_instance == nullptr)
            {
                _instance = new T;
            }
            return _instance;
        }
        static void deleteInstance()
        {
            if (_instance != nullptr)
            {
                delete _instance;
                _instance = nullptr;
            }
        }
    private:
        static T* _instance;
    
    };
    template<typename T>
    T* Singleton<T>::_instance = nullptr;

     之所以抽象一个模板类的单例模式就是为了复用代码,只需要把 想要单例化的类 继承自 以自己作为模板参数的Singleton就可以,比如要单例化SingletonA:

    // 实例化模板参数, CRTP递归模板
    class SingletonA :public Singleton<SingletonA>
    {
        //...一些其他的接口
    private:  //(项目中代码 构造函数变成了public 失去了单例的意义  尽管使用的时候都按照约定的 getInstance函数。)
        SingletonA() {};
        ~SingletonA() {};
      friend Singleton<SingletonA>;  // 这样基类中才能访问到派生类的私有的构造函数    
    
    
    };
    // 注意:如果某个类B 继承了SingletonA, B.getInstance()返回的还是 SingletonA的实例。

    这里有几个问题:

    1). CRTP递归模板,用一句话定义:  使用派生类 作为 模板参数 特化 基类

    2).

    如果基类中的数据成员为:

    static T _instance;

    CRTP这种模式可以么?可以,因为静态数据成员不占类的内存空间。

    3).

    如果基类中的数据成员为:

    T* _instance;

    CRTP这种模式可以么?可以,因为指针占固定的大小。

    4).

    如果基类中的数据成员为:

    T _instance;

    CRTP这种模式可以么?不可以,因为T还没有定义出来。

    5. 使用单例模式需要注意的问题:

    1>. 单例本质上是全局状态——它只是被封装在一个类中。他拥有全局变量造成的所有问题: 

      1). 理解代码成本,必须搜索,哪里修改过他,有没有改错。

      2). 增加耦合性, 因为全局可用。

      3). 对并行不友好:当我们创建了全局变量,一个所有线程都能过访问和修改的内存。当其中一个使用的时候,可能其他线程正在使用那块内存。

    在盲目使用具有全局作用域的单例对象之前,考虑有哪些访问对象的途径:

      1) 将对象作为参数传入函数。比如常用的渲染物体函数的 输入参数:图形设备对象context。

      2)从基类中获得。比如一个static变量在基类中定义。那么任何派生类都可以使用。这保证了继承体系之外的对象不能访问这个变量

      3)从已经是全局的东西中获取。用一个单例类 封装 一大推需要做成单例的类。

      4)服务定位器模式。 

    2>.只有在  "实现某类只能有一个实例,而且提供一个全局访问点" 的需求下,才用单例模式。

    实现:某类只实现一个实例,但是不提供全局访问点:

    这个类的创建 具有作用域

    class FileSystem
    {
    public:
      FileSystem()
      {
        assert(!instantiated_);
        instantiated_ = true;
      }
    
      ~FileSystem() { instantiated_ = false; }
    
    private:
      static bool instantiated_;
    };
    
    bool FileSystem::instantiated_ = false;
    FileSystem fileSys;   // 可以创建
    
    FileSystem fileSys1;    // 创建失败
    View Code

    3>.在游戏中使用单例模式:"在真正使用的时候才初始化"的危害

    比如一个音频系统的单例:

       1)如果初始化音频系统消耗了几百个毫秒,我们需要控制它何时发生。 如果在第一次声音播放时才初始化它自己,这初始化有可能发生游戏的高潮,导致可见的掉帧和断续的游戏体验。

      2) 如果初始化音频系统时需要在堆上分配内存,我们需要知道何时初始化发生了。因为 游戏通常需要严格管理在堆上分配的内存来避免碎片。在一开始的时候就初始化音频系统,我们就知道何时初始化发生了, 这样我们可以控制内存待在堆的哪里。

    所以我们选择在一开始的时候就初始化实例。

    4>.想想自己是真的需要单例模式,还是仅仅是简单的静态类。

    5>.游戏中的许多的单例模式都是管理器。但是管理器真的需要存在么?是不是可以把 管理器里的函数 放到 管理器 管理的类 的本身中?

    I’ve seen codebases where it seems like every class has a manager: Monster, MonsterManager, Particle, ParticleManager, Sound, SoundManager, ManagerManager. Sometimes, for variety, they’ll throw a “System” or “Engine” in there, but it’s still the same idea. 但是这些类真的都需要一个管理器么?

    Poorly designed singletons are often “helpers” that add functionality to another class. If you can, just move all of that behavior into the class it helps. After all, OOP is about letting objects take care of themselves.

    参考:

    《headfirst设计模式》

    《游戏编程模式》http://gpp.tkchu.me/singleton.html#为什么我们后悔使用它

     项目代码

  • 相关阅读:
    和为S的两个数字
    和为S的连续正数序列
    两个链表的第一个公共结点
    删除链表中重复的结点
    常用开发工具的安装(JDK、IDEA、Tomcat、Maven、Mysql和Nodepad++)——实习日志7.10
    蓄水池取样(转)
    prepare statement
    ProxySQL Getting started
    架构收录
    服务开机自启动
  • 原文地址:https://www.cnblogs.com/fulina/p/7029209.html
Copyright © 2020-2023  润新知