正确的姿势学习设计模式,设计模式必知必会(第二篇) --- 面试, 提升篇_(const singleton& obj)是什么意思

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

基础懒汉单例设计模式

内存回收懒汉单例设计模式

加锁懒汉单例设计模式(双检查加锁提升效率)

巧用static对象的懒汉单例设计模式

观察者设计模式

策略模式

责任链设计模式(数据结构模式)


单例设计模式(仅介绍懒汉式单例)

何为单例设计模式?

官方定义: 保证一个类仅有一个实例,并提供一个该实例的全局访问点。 ——《设计模式》GoF

口语表达:简单来说就是这个类只可以实例化出一个对象, 谓之单例.

如何可以达成一个类只能实例化出来一个对象的要求, 限制,  限制创建(构造), 根据一个类仅仅实例化一个对象的这个要求不难想到这个对象需要和类同生命周期, 所以这个唯一的对象需要是一个static对象, static 方法获取这个对象

(因为无法通过obj.GetInstance的方式获取,所以只能申明为static静态)

基础懒汉单例设计模式

class Singleton {
public:
    static Singleton* GetInstance() {
        if (nullptr == _instance) {
            _instance = new Singleton;
        }

        return _instance;//返回单例对象
    }
private:
    Singleton() {}//私有化构造函数
    Singleton& operator= (const Singleton& obj){}//禁止掉赋值
    Singleton(const Singleton& obj) {}//禁止掉拷贝构造
    static Singleton* _instance;//单例对象
};

Singleton* Singleton::_instance = nullptr;

上述这款懒汉式的单例设计模式, 问题分析:

  1. 对于_instance单例对象的回收问题, 我们new 出来的Singleton 对象何时释放的问题, static对象的回收不会自动调用delete,不会负责堆区内存的回收
  2. 针对多线程重入的问题, 多线程同时new多次的问题.

内存回收懒汉单例设计模式

针对内存回收问题的三种解决方案

  1. 智能指针
  2. 内部垃圾回收类回收
  3. 使用atexit函数做回收处理

内部类实现内存回收

//内部类进行垃圾回收
class Singleton {
public:
    static Singleton* GetInstance() {
        if (nullptr == _instance) {
            _instance = new Singleton;
        }

        return _instance;//返回单例对象
    }

private:
    class Garbage {
    public:
        ~Garbage() {
            if (_instance) {
                delete _instance;
                cout << "回收_instance对象" << endl;
                _instance = nullptr;
            }
        }
    };

    Singleton() {}//私有化构造函数
    Singleton& operator= (const Singleton& obj){}//禁止掉赋值
    Singleton(const Singleton& obj) {}//禁止掉拷贝构造
    static Singleton* _instance;//单例对象
    static Garbage garbage;
};

Singleton* Singleton::_instance = nullptr;
Singleton::Garbage Singleton::garbage;

注册atexit函数处理

class Singleton {
public:
    static Singleton* GetInstance() {
        if (nullptr == _instance) {
            _instance = new Singleton;
            atexit(Destrustor);
        }

        return _instance;//返回单例对象
    }

private:
    static void Destrustor() {
        if (_instance) {
            delete _instance;
            cout << "析构_instance" << endl;
            _instance = nullptr;
        }
    }
    Singleton() {}//私有化构造函数
    Singleton& operator= (const Singleton& obj) {}//禁止掉赋值
    Singleton(const Singleton& obj) {}//禁止掉拷贝构造
    static Singleton* _instance;//单例对象
};

Singleton* Singleton::_instance = nullptr;

加锁懒汉单例设计模式(双检查加锁提升效率)

//如何进行双检测加锁
class Singleton {
public:
    static Singleton* GetInstance() {
        if (nullptr == _instance) {
            //进来之后说明是还没有创建单例对象,加锁,
            unique_lock<mutex> lck(mtx);
            if (nullptr == _instance) {//加锁保证进来的仅仅只有一个线程.
                _instance = new Singleton;
                atexit(Destrustor);
            }
        }

        return _instance;//返回单例对象
    }

private:
    static void Destrustor() {
        if (_instance) {
            delete _instance;
            cout << "析构_instance" << endl;
            _instance = nullptr;
        }
    }
    Singleton() {}//私有化构造函数
    Singleton& operator= (const Singleton& obj) {}//禁止掉赋值
    Singleton(const Singleton& obj) {}//禁止掉拷贝构造
    static Singleton* _instance;//单例对象
    static mutex mtx;
};

Singleton* Singleton::_instance = nullptr;
mutex Singleton::mtx;

为何需要使用双重检测加锁. 保证仅仅只有一个线程new Singleton的情况下还可以减少加锁, 如果外围没有再多一层的if检测,可以吗,绝对也是OK的,但是存在很多的冗余加锁。因为仅仅只有第一次_instance == nullptr的时候是需要加锁new的,但是后面如果仅仅只是return _instance是完全没有必要枷锁的              —  故而采取双重检测

此种方式下已经算很完美了,但是还是存在问题.

— CPU指令重排的问题, 很可能导致_instance单例对象没有调用构造函数初始化以至于单例对象是随机值.

解决办法,参考CPU指令重排解决办法,内存屏障.

浅谈运行期间cpu指令重排 - 简书上篇谈到了编译器会进行内存操作指令的重排,这篇来谈谈运行期间cpu进行内存操作指令重排。当且仅当lock-free技术采用时才会出现这个情况,换句话说在多线程之间没有任何互斥…icon-default.png?t=N7T8https://www.jianshu.com/p/60e510737f20

#include <mutex>
#include <atomic>
class Singleton {
public:
    static Singleton * GetInstance() {
    Singleton* tmp = _instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//获取内存屏障
    if (tmp == nullptr) {
         std::lock_guard<std::mutex> lock(_mutex);
         tmp = _instance.load(std::memory_order_relaxed);
         if (tmp == nullptr) {
             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(std::memory_order_relaxed);
     if (nullptr != tmp) {
         delete tmp;
     }
 }
 Singleton(){}
 Singleton(const Singleton&) {}
 Singleton& operator=(const Singleton&) {}
 static std::atomic<Singleton*> _instance;
 static std::mutex _mutex;
};
std::atomic<Singleton*> Singleton::_instance;//静态成员需要初始化
std::mutex Singleton::_mutex; //互斥锁初始化

巧用static对象的懒汉单例设计模式

class Singleton {
public:
    ~Singleton() {
        cout << "析构对象_instance" << endl;
    }
    static Singleton& GetInstance() {
        static Singleton _instance;
        return _instance;
    }
private:
    Singleton() {}
    Singleton(const Singleton&) {}
    Singleton& operator=(const Singleton&) {}
};

如上的方式是完美解决了最初一款单例模式的所有问题的。 自动最后调用Destor回收内存,而且多线程访问也不会出现任何问题. 因为它仅仅只是第一次会init,后面都不会在创建对象. 利用了static静态对象的仅仅构造一次的优势.,      看吧写三次依旧仅仅只是调用一次构造

如上版本单例模式的优势所在

  1. 利用静态局部变量特性,延迟加载
  2. 利用静态局部变量的特性, 系统自动回收内存,自动调用析构
  3. 静态局部变量的初始化没有new操作带来的CPU指令reorder问题
  4. C++静态局部变量的初始化, 自带线程安全

这种单例设计模式是否已经无敌了,完美了,看上去好像是这样的。但是还是存在这样一个问题,一旦需要继承这个单例类,应该如何书写???    才可以使得可以通过继承的方式创建仍以具体类的单例对象

template<class T >
class Singleton {
public:
    static T& GetInstance() {
        static T _instance;//定义并且初始化单例对象
        return _instance;//return 单例对象
    }
protected:
    //一旦继承析构函数一定需要进行虚析构
    virtual ~Singleton() {
        cout << "析构单例对象" << endl;
    }
    Singleton() {
        cout << "构造单例对象" << endl;
    }
    Singleton(const Singleton&) {}//隐藏拷贝构造
    Singleton& operator= (const Singleton&) {}//隐藏赋值重载
};


//继承书写, 通过如此形式实现继承单例类的书写, 
//如此可以实现了任意类型的单例类实例化
class DesignPattern : public Singleton<DesignPattern> {
    friend class Singleton<DesignPattern>;
    //声明为友元, 使得Singleon<DesignPattern>类可以访问private成员
private:
    DesignPattern() {
        cout << "调用DesignPattern构造" << endl;
    }//私有化构造函数
    DesignPattern& operator=(const DesignPattern&) {}//私有化赋值重载
    DesignPattern(const DesignPattern&){}//私有化拷贝构造
    virtual ~DesignPattern() {
        cout << "调用DesignPattern析构" << endl;
    }
};

观察者设计模式

官方定义: 定义对象间的一种一对多(变化)的依赖关系,以便当一个对象(Subject)的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。 ——《 设计模式》 GoF

口语解释:  说白了就是定义一个类对象跟多个其他类对象之间的一种依赖关系, 其他多个类对象依赖于这一个类对象. 一旦这一个类对象状态发生了改变.   其他依赖这个对象的所有其他类对象(观察者)  都会得到通知进行自动更新变化

通知依赖关系——一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知。

怎么说:这个强依赖关系, 过于的耦合了, 我们需要实现运行时绑定, 晚绑定, 依赖抽象,依赖倒置来解除这个对具体对象的强依赖. 改为对抽象的依赖.

我们还是通过前后对比书写的方式来重构研究一下具体缺乏哪些设计原则, 添加哪些设计原则重构最终实现了观察者模式

场景引入:  比如说存在这样一个场景,正常情况下,大家上班都在进行摸鱼防水,轻松, 但是大家也都是很聪明的存在, 是懂得观察的存在. 要是老板来了,大家就会立刻Update更新当前的状态,所作的事情.

—    此时的所有Wocker  或者说Staff 全部都是监视者.   而老板就是这个被监视的对象,老板的状态改变就会引起所有监视者的状态改变

class Staff;
class Boss {
private:
    string action;
public:
    Boss(string _action): action(_action) {}
    string& GetAction() {
        return action;
    }

};

class Staff {
private:
    string name;
    Boss boss;
public:
    Staff(string _name, Boss _boss)
        : name(_name)
        , boss(_boss) {
    }

    void Run() {
        if (boss.GetAction() == "老板从大门走进来了") {
            cout <<"name: " << name << "老板来了,"<< "立刻马上认真工作" << endl;
        }
        else if (boss.GetAction() == "老板走出去了") {
            cout <<"name: " << name << "老板走了,"<< "立马开始摸鱼起来了" << endl;
        }
    }
};

int main() {
    Staff st("张三", Boss("老板走出去了"));
    st.Run();
    Staff st2("李四", Boss("老板从大门走进来了"));
    st2.Run();
    return 0;
}

如上代码可行不可行, 其实也可以实现功能. 老板回来了,出去了老板的状态可以影响到它的不同员工的状态.

但是上述代码用一句名人说的话就是  bad smell. 嗅到了坏味道.  why?

员工类高度耦合依赖Boss老板类, 一旦老板类的状态发生任何变化都可能导致员工类无法正常使用, 另外没有遵循设计原则.     不遵循依赖倒置, 是编译时绑定依赖, 而不是运行时依赖.   毫无扩展性可言, 比如说需要通知一下其他的事情, 状态,便需要修改类. 而不是扩展类.   没有做到封装可能的变化点

改进代码如下:

class Observer {
public:
    //Subject目标变化带来的Update
    virtual void Update(int) = 0;
protected:
    virtual ~Observer() {}
};


//目标,被观察者
class Subject {
public:
    void Attach(Observer* pob) {
        pobs.push_back(pob);
    }

    void Detach(Observer* pob) {
        auto it = find(pobs.begin(), pobs.end(), pob);
        if (it != pobs.end()) {
            it = pobs.erase(it);
        }
    }

    void Notify() {
        auto it = pobs.begin();
        while (it != pobs.end()) {
            (*it)->Update(action);
            ++it;
        }
    }
    virtual void SetAction(int action) = 0;
protected:
    int action;
    vector<Observer*> pobs;
    virtual ~Subject(){}
};


class Boss : public Subject {
public:
    //action 0 表示老板离开, 1 表示老板进入
    virtual void SetAction(int action) {
        this->action = action;
        Notify();
    }
};

class Staff : public Observer {
public:
    virtual void Update(int action) {
        if (action == 0) {
            cout << "老板走了大家可以划水了" << endl;
        } 

        if (action == 1) {
            cout << "老板进来了, 大家快好好工作" << endl;
        }
    }

};

对比前后,上述设计模式采用了哪些设计原则以及有哪些特征

  1. 开闭原则, 对扩展开放, 对修改封闭
  2. 依赖倒置原则, 都不是依赖具体的类, 而是依赖抽象, 将编译时依赖变成了运行时依赖 (解决了具体类之间的强耦合性)                 —  封装变化点
  3. 使用面向对象的抽象,Observer模式使得我们可以独立地改变目标与观察者,从而使二者之间的依赖关系达致松耦合。
  4. 目标对象不需要知道谁订阅了自己,也就是不需要知道哪些对象在观察自己
  5. 观察者自己决定是否订阅通知,目标对象并不关注谁订阅了
  6. 在目标对象状态改变的时候通知观察者的顺序不固定, 随机
  7. Observer模式是基于事件的UI框架中非常常用的设计模式,也是MVC模式的一个重要组成部分。

观察者模式的特点总结

  1. 观察者模式使得我们可以独立地改变目标与观察者,从而使二者之间的关系松耦合
  2. 观察者自己决定是否订阅通知,目标对象也不关系谁订阅了

观察者模式的记忆技巧

一个对象状态变化引发的联动现象      ----    状态改变, 触发联动

策略模式

官方定义: 定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用它的客户程序而变化。 ——《设计模式》 GoF

口语解释:封装算法流程. 各个算法之间是等效的, 抽象算法特征进行扩展,   核心关键在于使得算法可以独立于客户程序变化 (解耦合:编译时依赖转换为运行时依赖.)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

----    状态改变, 触发联动**

策略模式

官方定义: 定义一系列算法,把它们一个个封装起来,并且使它们可互相替换。该模式使得算法可独立于使用它的客户程序而变化。 ——《设计模式》 GoF

口语解释:封装算法流程. 各个算法之间是等效的, 抽象算法特征进行扩展,   核心关键在于使得算法可以独立于客户程序变化 (解耦合:编译时依赖转换为运行时依赖.)

[外链图片转存中…(img-lyfGIw8w-1715826128840)]
[外链图片转存中…(img-YalkPksg-1715826128840)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值