本文用于个人总结设计模式——C++中的相关知识
依赖倒转原则
- 高层模块不应该依赖低层模块,两个都应该依赖抽象
- 抽象不应该依赖细节,细节应该依赖抽象
什么意思呢:
- 高层模块:业务层的实现
- 低层模块:底层接口,如封装好的API、动态库等
- 抽象:可以理解为抽象类(但不一定是纯虚),即接口
- 细节:实现细节
- 里氏代换原则:子类类型必须能替换掉它们的父类类型,即可以用父类指针指向子类对象,父类中不能存在子类没有的属性
意思就是:
- 高层模块不应该直接调用低层模块,应当在其中添加抽象类(虚函数),再用抽象类的子类去实现相关调用。这样在相关调用发生变化时,大大减少改动代码量。
- 细节是通过多态在子类重写父类虚函数的时候实现的,需要满足里氏代换原则。
单例模式
懒汉模式的相关问题
可以加锁来解决懒汉模式的线程不安全问题
懒汉模式加锁导致的顺序访问低效问题:使用双重检查锁定:
static TaskQueue* getInstance()
{
if (m_taskQ == nullptr)
{
m_mutex.lock();
if (m_taskQ == nullptr)
{
m_taskQ = new TaskQueue;
}
m_mutex.unlock();
}
return m_taskQ;
}
在这种情况下,底层仍容易出现问题:
由于new
的过程在底层二进制中分为三步实现:申请一块空内存 → 创建对象并写入内存 → 将内存地址传递给指针。但机器在执行指令时会重排指令,若先执行了1、3步才执行2,则导致指针指向了一块没有写入内容的内存,使用该指针的线程就挂了。
怎么解决呢?
使用 C++11 引入的原子变量atomic
这样真的非常麻烦,所以推荐使用下面的方法来实现懒汉模式:
使用静态的局部对象解决线程安全问题
class TaskQueue
{
public:
// = delete 代表函数禁用, 也可以将其访问权限设置为私有
TaskQueue(const TaskQueue& obj) = delete;
TaskQueue& operator=(const TaskQueue& obj) = delete;
static TaskQueue* getInstance()
{
static TaskQueue taskQ;
return &taskQ;
}
private:
TaskQueue() = default;
};
int main()
{
TaskQueue* queue = TaskQueue::getInstance();
return 0;
}
C++11 标准引入了对静态局部变量初始化的线程安全保证。具体来说,标准规定:
- 如果多个线程同时访问一个静态局部变量,并且该变量尚未初始化,则只有一个线程会执行初始化,其他线程会等待初始化完成。
- 初始化完成后,所有线程都可以安全地访问该变量。
饿汉模式的线程安全问题
饿汉模式的单例对象不存在线程安全问题
但多线程访问单例对象的内部数据时可能会出现线程安全问题
使用互斥锁保护可能发生线程安全问题的成员变量。
工厂模式
简单工厂模式
// 产品父类(虚)
// 产品子类1
// 产品子类2
// 工厂类(在此new对象)- 输入为想要的产品
// 通过switch-case生产父类指针的子类对象
用多态实现。子类继承基类,基类的析构函数应该是虚函数,这样才能够通过父类指针或引用析构子类的对象。工厂类返回基类类型的指针,保存的是子类对象的地址,所以实际调用的是子类对象中的函数。
switch-case
语句明显违背了封闭原则,仍需要更改。
工厂模式
扩展工厂类,产品父类变成工厂父类,产品子类变成工厂子类
// 工厂父类(虚)
// 工厂子类1(在此new对象)- 不需要参数
// 工厂子类2(在此new对象)
依旧是多态实现。每个工厂返回的还是工厂父类的基类类型。
抽象工厂模式
// 工厂父类1(虚)
// 工厂子类1-1
// 工厂子类1-2
// 工厂父类2(虚)
// 工厂子类2-1
// 工厂子类2-2
// 工厂父类3(虚)
// 工厂子类3-1
// 工厂子类3-2
// 抽象工厂类(虚)
// 子工厂类(new 1-1 2-1 3-1的产品)
// 子工厂类(new 1-2 2-2 3-2的产品)
// 子工厂类(new 1-3 2-3 3-3的产品)
建造者模式(生成器模式)
生成器
将建造函数抽离出来,放到“生成器”独立类中。父类生成器中的建造函数应该设置为虚函数。只需要选择需要的生成器步骤并调用,就可以得到满足需求的实例对象。
生成器类内需要提供重置函数(构造函数也会调用这个重置函数),目的是能够使用生成器生成多个对象。
void reset() override {
m_product = new Prodect;
}
同时也提供get函数,将生成的对象送给外部。
主管类
用于定义创建步骤的执行顺序, 程序中并不一定需要主管类。 客户端代码可直接以特定顺序调用创建步骤。 不过, 主管类中非常适合放入各种例行构造流程, 以便在程序中反复使用。
// 目标对象类
// 父类生成器(虚)
// 子类1生成器,拥有多个方法构造不同部件
// 子类2生成器,拥有多个方法构造不同部件
// 主管类(传入父类生成器对象,但实参是子类对象)
// 主管类方法1:调用子类1生成器中的方法1-1 1-2
// 主管类方法2:调用子类1生成器中的方法1-1 1-2 1-3
原型模式
原型模式就是能够复制已有的对象。类的拷贝构造函数只允许复制一个当前类的对象,若想用父类指针拷贝一个子类对象则会失败。原型模式可以通过已有的子类对象克隆父类指针的子类对象。
用克隆函数实现。
// 父类
// 克隆函数(虚)
// 子类1
// 克隆函数(return new 子类1(*this);)
// 子类2
// 克隆函数(返回子类对象)
// 子类对象A
// 父类* B = A->clnoe();
子类的clone()
函数体内部是通过当前子类的拷贝构造函数复制出了一个新的子类对象。
适配器模式
将一个类的接口转换成用户希望的另一个接口。
在使用适配器类为相关的类提供适配服务的时候,如果这个类没有子类就可以让适配器类继承这个类,如果这个类有子类,此时使用继承就不太合适了,建议将适配器类和要被适配的类设置为关联关系。