设计模式基础&单例模式

一、相关基础

1.为什么要有设计模式?

模式是在特定环境下人们解决某类重复出现问题的一套成功或有效的解决方案。【A pattern is a successful or efficient solution to a recurring problem within a context】
作用:(站在巨人的肩膀上)可重用代码,增加代码可读性以及保证代码可靠性。

2.设计模式的组成

我认为可以按照STAR法则去处理:

  • Situation: 在何种环境或者约束条件下使用
  • Task: 待解决的问题是什么
  • Action: 如何解决
  • Result: 有哪些效果(优缺点),注意事项

3. 设计模式的分类(23种)

  • 构建型模式: 主要用于描述如何创建对象

单例模式,简单工厂模式,工厂方法模式,抽象工厂模式,原型模式,建造者模式

  • 结构型模式: 主要用于描述如何实现类或对象的组合

适配器模式,桥接模式,组合模式,装饰模式,外观模式,享元模式,代理模式

  • 行为型模式: 主要用于描述类或对象怎样交互以及怎样分配职责

职责链模式,命令模式,解释器模式,迭代器模式,中介者模式,备忘录模式,观察者模式,状态模式,策略模式,模板方法模式,访问者模式

4. 软件设计的七大原则

这七种设计原则在设计模式中的使用是有取舍的,并不是每一种设计模式都完全遵循七大原则

  1. 开闭原则
    定义:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭,这是面向对象最基础的设计模式。
    核心思想:用抽象构建框架,用实现扩展细节
    比如淘宝购物节价格打八折,商品销售类的接口不动,额外增加一个新的类,类中实现接口的getPrice()方法改变
  2. 依赖倒置原则
    定义:高层模块不应该依赖低层模块,二者都应该依赖其抽象
    针对接口编程,不要针对实现编程
  3. 单一职责原则
    定义:不要存在多于一个导致类变更的原因
    一个类/接口/方法只负责一项职责
  4. 接口隔离原则
    定义:用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口
  5. 迪米特法则(最少知道原则)
    定义:一个对象应该对其他对象保持最少的了解。又叫最少知道原则。尽量降低类与类之间的耦合
  6. 里氏替换原则
    定义:如果对每一个类型为T1的对象o1,都有类型为T2的对象02,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
    定义扩展:
    一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明地使用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。
    引申意义:子类可以扩展父类的功能,但不能改变父类原有的功能。
  7. 合成/复用原则(组合/复用原则)
    定义:尽量使用对象组合/聚合,而不是继承关系达到软件复用的目的

详细版推荐链接:https://chenxiao.blog.csdn.net/article/details/91411528

二、单例原则

1.相关概念

单例模式:一个类只允许创建一个实例,且提供了全局访问的方法。

  • Situation: 系统只需要一个实例对象,比如配置信息类;或者客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点(比如win中任务管理器只能启动一个),不能通过其他途径访问该实例。真实情景下,比如web服务器的日志系统。
  • Task: 为解决一个全局使用的类频繁地创建和销毁
  • Action : 提出单例模式,饿汉型和懒汉型创建
  • Result :
    • 优点:内存中只有一个实例,减少内存开销,避免频繁地创建销毁实例; 避免对资源的多重占用(比如写文件操作)
    • 缺点:没有接口,不能继承,与单一职责原则冲突。

2.实现

思想:私有化构造函数,避免外部通过new创建实例。getInstance()判断系统是否有了这个单例,如果有则返回,否则则创建。

饿汉式构造

思想:饿地迫不及待,初始化即实例化

class Singleton {
public:
    //创建, 线程安全
	static Singleton* getInstance(){
        return instance;
    }
private:
	Singleton(){};
	static Singleton * instance;
};
//关键点:初始化即实例化
Singleton* Singleton::instance = new Singleton();  //需要自行销毁

int main()
{
    Singleton * instance = Singleton::getInstance();
    delete instance;
    instance = nullptr;
    delete instance;   //这个不报错

    return 0;
}

为什么这种是线程安全的?
全局静态实例初始化是在程序开始时进入主函数之前就由主线程以单线程地方式完成初始化,对象只生成一次,所以是线程安全的。
有什么缺点?
但饿汉方式不论是否需要使用该对象都将其定义出来,浪费了内存,或者减慢了程序的启动速度。所以使用懒汉模式进行优化
instance指向的空间什么时候释放呢?
上例需要coder自行销毁。
程序在结束的时候,系统会自动析构所有的全局变量。事实上,系统也会析构所有的类的静态成员变量,就像这些静态成员也是全局变量一样。所以利用这个特征,我们可以在单例类中额外定义一个静态成员变量,而它的唯一工作就是在析构函数中删除单例类的实例。第二种较为简单的就是直接把instance从指针类型改成静态实例对象

class Singleton {
public:
//返回引用
	static Singleton& getInstance(){
        return instance;
    }
private:
	Singleton(){}
    static Singleton  instance;
};
Singleton Singleton::instance;

补充:全局变量、文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量(一般为函数内的静态变量)在第一次使用时分配内存并初始化。这里的变量包含内置数据类型和自定义类型的对象。

懒汉式创建

思想:懒汉拖延,什么时候用,什么时候再创建。这种需要时再加载实例的技术也称为延迟加载技术。不是线程安全的,为了避免多个线程同时调用getInstance()方法创建实例,我们可以使用锁或者static变量

加锁:Double-Checked Locking Pattern (DCLP)
#include<iostream>
#include <mutex>
using namespace std;

class Singleton {
public:
	static Singleton* getInstance(){
        if(instance == nullptr){
            std::lock_guard<std::mutex> lck(m_mutex);
            if(instance == nullptr){
                instance = new Singleton();
            }
        }
        return instance;
    }

    /* 这种销毁模式不是线程安全的(但没有测试)
    static void delInstance() {
        if(instance != nullptr) {
            delete instance;
            instance = nullptr;  //记得加这句,避免野指针的出现
        }
    }*/
   //同理,实现线程安全,防止多次delete
    static void delInstance() {
        if(instance != nullptr) {
            std::lock_guard<mutex> lck(m_mutex);
            if(instance != nullptr){
	            delete instance;
	            instance = nullptr;  //记得加这句,避免野指针的出现
	        }
        }
    }
private:
	Singleton(){};
    ~Singleton(){};
    static std::mutex m_mutex;
    static Singleton * instance;
};
std::mutex Singleton::m_mutex;
Singleton * Singleton::instance = nullptr;


为什么需要两次检查?
只检测一次,即保留second-check, 在每次调用获取实例时都需要加锁,影响程序性能。只保留first-check, 可能会发生多线程同时创建实例的情况。双层检测仅在第一次创建单例时加锁,其他都不符合null==p的情况,直接返回创建好的实例。。

Souble-Check是否存在问题?

《C++ and the Perils of Double-Checked Locking》这篇文章中提到:
m_pInstance = new Singleton;
这条语句实际上做了三件事,第一件事申请一块内存,第二件事调用构造函数,第三件是将该内存地址赋给instance_。
但是不同的编译器表现是不一样的。可能先将该内存地址赋给m_pInstance,然后再调用构造函数。这是线程A恰好申请完成内存,并且将内存地址赋给m_pInstance,但是还没调用构造函数的时候。线程B执行到语句1,判断m_pInstance此时不为空,则返回该变量,然后调用该对象的函数,但是该对象还没有进行构造。(来源:https://zhuanlan.zhihu.com/p/396933580)。 可以考虑用atomic 原子操作实现。

同时它也需要手动释放内存。

使用静态局部变量:
class Singleton {
public:
	static Singleton* getInstance(){
        //局部静态变量
        static Singleton  instance;  //错误写法:  static Singleton * instance = new Singleton() ;new出来的对象需要自行释放 
        return &instance;
    }
private:
	Singleton(){}
};

为什么用静态局部变量可以保持线程安全?

对于local static 对象,其初始化发生在控制流第一次执行到该对象的初始化语句时。多个线程的控制流可能同时到达其初始化语句。
在C++11之前,在多线程环境下local static对象的初始化并不是线程安全的。具体表现就是:如果一个线程正在执行local static对象的初始化语句但还没有完成初始化,此时若其它线程也执行到该语句,那么这个线程会认为自己是第一次执行该语句并进入该local static对象的构造函数中。这会造成这个local static对象的重复构造,进而产生内存泄露问题。所以,local static对象在多线程环境下的重复构造问题是需要解决的。
而C++11则在语言规范中解决了这个问题。C++11规定,在一个线程开始local static 对象的初始化后到完成初始化前,其他线程执行到这个local static对象的初始化语句就会等待,直到该local static 对象初始化完成

补充:静态局部变量:
静态局部变量存在在内存的全局数据区,作用域是定义它的函数。静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化;静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为 0;函数结束时,静态局部变量不会释放,每次该函数调用 时,也不会为其重新分配空间。它始终驻留在全局数据区,直到程序运行结束。静态变量会自动初始化

参考链接

线程安全,内存释放:https://zhuanlan.zhihu.com/p/396933580
线程安全的单例模式(利用local static):https://blog.csdn.net/lgfun/article/details/105810039
设计模式:https://blog.csdn.net/leacock1991/article/details/111713017

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值