设计模式(C++)笔记与总结
前言
你好,我是苏青羽,是一名默默无闻的计算机爱好者。
本笔记是我在学习设计模式时所总结的学习笔记,主要记录了设计模式的一些基础知识点,在这里分享给大家,希望通过该笔记能帮助更多的人去理解设计模式。(本笔记代码均由C++实现)
笔记中参考了网上的很多博客、公众号、网课、面经等等,这些均在笔记末尾的参考资料中有所展示,大家可自行查看,做更深入的了解。
本笔记主要用于在学习上的复盘,并不适合初学者学习。如果在笔记中遇到了不懂的知识点,一定要去自己看书或者上网查询相关知识点!
如果发现本文有较大的硬性错误、或者是某些内容存在侵权、以及有什么想补充的问题和内容,请点击“关于”,通过里面预留的联系方式同我联系!
本笔记正在实时更新中,若是想获取笔记的最新PDF版以及了解关于我的更多信息,可扫描下方二维码,或微信搜索公众号“苏青羽”关注我!
前提基础
学习本笔记前,请事先掌握以下基础知识:
- C++基础
设计模式概述
1. 什么是设计模式?
设计模式(Design Pattern)是一套被反复使用、被多数人所知晓的代码设计经验总结。使用设计模式是为了提高代码重用性和可靠性,让代码更容易被他人理解。一个完整的设计模式一般有以下几个元素:模式名称、问题、解决方案和效果。
2. 设计模式可以被用在哪种语言上?
设计模式不局限于某种实际的编程语言,只要该语言满足面向对象的特性,即:封装、继承、多态,那么就可以使用设计模式,包括但不局限于C++、Java。本文的代码均以C++为例。
3. 什么是类和对象?
类是一些相关的数据和方法的集合,而对象是类的一个实例。
4. 类/对象之间的关系有多少种?
(1) 继承:让某个类获得另一个类的属性和方法,是一种类与类之间的关系。通常这两个类会被分为父类和子类,他们的关系在编译时期就被确定下来,是一种静态关系。
(2) 组合:让某个对象包含和使用另一个对象,是一种对象与对象之间的关系。通常这两个对象的关系会在运行时发生变化,是一种动态关系。
5. 设计模式的分类?
(1) 创建型模式:主要用于创建对象。
(2) 结构型模式:主要用于将类或对象结合在一起形成功能更强大的新结构。
(3) 行为型模式:主要关注对象的行为和相互作用,将对象的功能抽象化。
6. 什么是设计模式的原则?
所有的设计模式底层都遵循了一些原则,其中包括:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、合成复用原则。这些原则比设计模式本身更加重要,只要在开发过程中遵循某个或几个原则,代码就能获得极大的重用性和可靠性。
7. 说说设计模式的单一职责原则(Single Responsibility Principle)?
单一职责原则就是对一个类而言,应该仅有一个引起它变化的原因,也就是该类应该只负责某个功能。如果一个类承担的职责过多,就等于把这些职责耦合到了一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力,导致设计变得脆弱。
8. 说说设计模式的开闭原则(Open-Closed Principle)?
类虽然封装了代码,但在很多情况下无法做到完全封闭,后续可能会对类功能进行修改与扩展。开闭原则要求类的设计者要对类后续可能在哪些位置发生变化了然于心,然后在这些位置上创建虚函数。后续对类功能的修改和扩展只能通过新类的继承与重写虚函数来实现,不应该在已经成熟的类中做修改,以免破坏整体的代码结构。
9. 说说设计模式的里氏替换原则 (Liskov Substitution Principle)?
里氏替换原则是对开闭原则的补充,它规定了在任意父类可以出现的地方,子类都一定可以出现。也就是说,子类可以重写父类的虚函数来进行修改与扩展,但这不应该影响父类方法本身的含义和功能。
10. 说说设计模式的依赖倒置原则 (Dependence Inversion Principle)?
(1) 依赖倒置原则指的是:高层模块不应该依赖低层模块,两个都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。其中的高层模块可以理解为上层应用,低层模块可以理解为是一些库提供的底层API接口,而抽象即是指C++中的抽象类。
(2) 我们在设计代码时,应该设计一个中间的抽象类,在它的各个具体子类中封装不同的实现细节,也就是使用不同的底层API接口,而让上层应用统一使用抽象类来完成各种功能。这样一来这个抽象类就成了项目架构里高层与低层的桥梁,它将二者整合到一起,但又对二者屏蔽了对方的细节,大大提高了整个项目架构的稳定性和扩展性。
11. 说说设计模式的接口隔离原则(Interface Segregation Principle)?
接口隔离原是指将不同的功能定义在不同的接口(类函数)中,一个接口只做一件事,以此实现接口的隔离。这样可以减少接口之间依赖的冗余性和复杂性,实现高内聚和低耦合。
12. 说说设计模式的迪米特法则(Law Of Demeter)?
迪米特法则是指一个对象应该尽可能少地与其他对象发生相互作用,减少对象间不必要的依赖。它的核心思想在于降低对象间的耦合度,提高对象的内聚性,避免代码变得臃肿。
13. 说说设计模式的合成复用原则(Composite/Aggregate Reuse Principle)?
合成复用原则是指尽可能通过组合来实现类的功能复用和扩展,而不要使用继承来扩展类的功能。相比起继承,组合具有更大的灵活性和动态变化的能力,可以防止继承泛滥,降低代码的维护成本。
创建型模式
1. 什么是单例模式(Singleton)?
在一个项目中的全局范围内,某个类的对象实例有且仅有一个,其他模块仅能通过这个唯一实例来访问数据和调用功能,即单例模式。根据实现不同又分为饿汉式和懒汉式。
2. 如何实现单例模式(饿汉式)?
在类加载的时候就立刻实例化,也就是在未进入main函数前,程序就会初始化一个单例对象,后续所有操作就只会使用这个单例对象。它的实现方式就是将构造函数私有化,禁止显式构造,外部只能通过一个类静态函数来获取类中的静态唯一实例,而这个实例对象则放在类外直接初始化。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
3. 单例模式(饿汉式)的优缺点?
(1) 优点:是最简单的一种单例形式,没有添加任何的锁,执行效率最高,且线程安全。
(2) 缺点:对象在未被使用时就被分配内存并初始化,这可能会造成内存浪费。由于不知道编译器对全局对象的初始化顺序,如果试图在其他全局对象的初始化操作中去使用单例对象,很可能因为单例对象还没有被初始化,导致出现难以排查的错误。
4. 如何实现单例模式(懒汉式)?
在类加载的时候不实例化,而是在需要使用的时候再创建单例对象,后续所有操作就只会使用第一次创建的单例对象。它的实现方式类似于饿汉式,但其单例对象会在类外会初始化为空值(NULL),真正创建单例对象的时机延后到第一次调用实例返回函数时。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
5. 单例模式(懒汉式)的优缺点?
(1) 优点:内存利用率高,对象直到要使用才创建,也不用考虑全局对象的初始化顺序问题。
(2) 缺点:存在线程不安全问题,在多线程环境下,由于创建对象的过程在底层不是原子的,可能导致多个线程同时创建多个单例对象。虽然可以用线程的互斥同步机制来解决,但也因此导致了懒汉式的执行效率低下。
6. 如何利用锁解决单例模式(懒汉式)的线程安全问题?
(1) 单检查锁:在创建单例对象时加上互斥锁,阻止其他线程创建。它强制线程在获取单例对象时,只能串行获取,而不管单例是否已经存在,失去了并发性,效率低下。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
(2) 双检查锁:以单检查锁为基础,在被锁住的代码块的外层添加额外的检查判断。当对象被创建后,其他线程通过第一次检查就可以直接跳过加锁解锁,效率较高。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
7. 单例模式(懒汉式)的双检查锁有什么缺陷?
双检查锁的问题仍然出在创建对象过程的非原子性上。在创建对象时,底层的机器指令会分为三步:预分配对象所需内存->构造对象->对象指针赋值。为了优化执行效率,这三步可能会被重排序,比如预分配对象所需内存->对象指针赋值->构造对象。如果在前两步执行完之后就切换线程,那么当前线程会在第一次检查时直接跳过加锁解锁过程,转而返回一个野指针,它指向了一个没有被初始化的对象,这无疑十分危险。
8. 如何彻底解决单例模式(懒汉式)的线程安全问题?
我们可以将单例对象设置为类函数的静态局部变量,而不是类的静态成员变量。在C++11标准下,编译器在底层会保证静态变量初始化的原子性。虽然创建对象是非原子的,但对象初始化是原子的,那么直接在初始化时创建对象就可以了,同时也不需要任何的锁机制了。当然,为了与饿汉式区分开,类静态成员变量在初始化并不能创建对象,那么此时使用类函数的静态局部变量就变得理所当然了。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
9. 如何销毁单例模式产生的单例对象?
(1) 单例对象一般不能由某个线程主动销毁,这可能导致其他使用该对象的线程发生错误。
(2) 通常单例对象是等待程序结束后由系统自动回收,但是这样的方式并不会调用单例对象的析构函数。一般推荐在单例类的内部,创建一个专门用于销毁单例对象的辅助类,它会在程序结束时调用单例对象的析构函数并销毁单例对象,如下所示。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
(3) 以上的前提都是单例对象必须是在堆中创建的,这意味着我们需要显式地使用delete才能完整地释放它。如果是以类函数的局部变量方式创建单例对象,则不用这么麻烦,程序会帮我们管理所有的静态变量,在程序结束时会安全且完整地释放它。
10. 什么是工厂模式(Factory)?
工厂模式在创建对象时,不直接new,而是交由一个工厂类负责创建。它将创建对象的代码统一放在工厂类管理,让对象的使用者不必关心创建对象的具体逻辑,解耦了对象使用者和对象创建者的依赖关系。根据实现不同又分为简单工厂、工厂模式和抽象工厂。
11. 如何实现简单工厂(Simple Factory)?
创建一个新的非抽象类——工厂类,它给外部提供了公共的成员函数用来创建产品对象。所有具体的产品类都继承自同一个产品抽象基类,并拥有自己的具体实现。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
12. 简单工厂(Simple Factory)的优缺点?
(1) 优点:实现简单,用途广泛,大部分问题用简单工厂就可以解决。
(2) 缺点:违背开闭原则,新增产品时需要更改工厂类中的对象创建函数,扩展性差。
13. 如何实现工厂模式(Factory)?
工厂模式在简单工厂的基础上,将工厂类改造为抽象基类,它定义了创建产品对象的纯虚函数,而将具体的创建细节交由工厂子类去实现,不同的工厂子类负责创建不同的产品对象。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
14. 工厂模式(Factory)的优缺点?
(1) 优点:解耦了产品和工厂的依赖关系,遵循开闭原则,更具扩展性。
(2) 缺点:工厂模式创建出来的产品对象相对简单,难以应用在更复杂的场景下。
15. 如何实现抽象工厂(Abstract Factory)?
抽象工厂在工厂模式的基础上,将产品类改造为普通类,它里面包含了数个零件,每个零件都是继承自某个零件抽象基类的具体零件,而零件组装成产品的过程则由工厂子类负责。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
16. 抽象工厂(Abstract Factory)的优缺点?
(1) 优点:可创造出更复杂的对象,有着优秀的可扩展性。
(2) 缺点:代码复杂度较高,一般用于非常复杂的项目环境下。
结构型模式
1. 什么是适配器模式(Adapter)?
适配器模式在不改变原本类函数功能的情况下,改造某个类的接口,使原本不兼容的对象能够相互兼容,相当于是对象间的桥梁。根据实现不同又分为类适配器和对象适配器。
2. 如何实现类适配器(Class Adapter)?
创建一个适配器类,它可以将某个类的函数接口转换为另一个类的函数接口。其方法是让它继承它需要服务的两个类,然后重写目标类函数,在其中直接调用源类函数即可。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
3. 类适配器(Class Adapter)的优缺点?
(1) 优点:实现简单,使用方便。
(2) 缺点:编程语言必须支持多继承,而且容易导致继承关系复杂。
4. 如何实现对象适配器(Object Adapter)?
对象适配器在适配器类的实现上有所不同,适配器类只继承目标类,然后通过组合的方式保存源类对象指针。在重写目标类函数时,通过源类对象指针来间接调用源类函数。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
5. 对象适配器(Object Adapter)的优缺点?
(1) 优点:通过组合的方式实现适配器,被大多数面向对象的语言支持,更加通用和灵活。
(2) 缺点:在实现方式上略有复杂,需要涉及较多的对象。
6. 什么是装饰器模式(Decorator)?
装饰者模式可以给一个对象动态地添加新的功能,同时又不改变其结构。它实现了原功能的扩展,提供了比继承更有弹性的替代方案。
7. 如何实现装饰器模式(Decorator)?
装饰模式定义了一个抽象基类——组件类,其子类有具体组件类和装饰者类。具体组件类会重写父类虚函数,实现具体功能。装饰者类通过组合的方式保存了一个组件类对象指针(既可以是具体组件类也可以是装饰者类)作为被装饰者,它并不会重写父类虚函数,因此它也是个抽象基类。装饰者的子类会重写父类虚函数,在被装饰者的外部添加额外实现。因此,每个装饰者都可变为被装饰者,被一层一层地添加功能并迭代下去。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
8. 装饰器模式(Decorator)的优缺点?
(1) 优点:相比起单纯的继承,它更具备弹性和灵活度,同时又不会增加太多的子类。
(2) 缺点:装饰者太多容易增加代码复杂度,还可能因为装饰而抹除某类对象的特殊属性。
9. 什么是外观模式(Facade)?
外观模式可以封装复杂的代码底层逻辑,只提供使用者关心的上层接口。
10. 如何实现外观模式(Facade)?
外观模式创建一个上层应用类,它调用了项目中各种类对象和函数,但是对用户隐藏具体的调用细节,而是提供了一个个简单函数接口供用户使用。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
11. 外观模式(Facade)的优缺点?
(1) 优点:化繁为简,用户不必关心复杂的底层实现逻辑,使用方便,应用广泛。
(2) 缺点:用户无法实现自定义功能,只能交由应用设计者进行迭代升级。
12. 什么是代理模式(Proxy)?
代理模式为某种对象提供一种外部代理,以控制其他对象对它的访问,相当于是一个中介。
13. 如何实现代理模式(Proxy)?
代理模式创建了一个抽象基类,其子类有被代理类和代理类,它们有着共同的函数接口,因此对外界来说两者的使用方式是相同的。被代理类实现了功能的所有运行逻辑,代理类则通过组合的方式保存并管理被代理类对象,以此控制外部对被代理对象的访问和操作。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
14. 代理模式(Proxy)的优缺点?
(1) 优点:降低对象间的耦合度,对象间的交流需要通过代理来传递,外界无法得知对象内部的信息,甚至可以做到一个对象通过一个代理来与多个对象进行交互,有着较高的安全性、扩展性和维护性。
(2) 缺点:增加了代码复杂度,对象间的交流效率可能会因为代理而降低。
15. 适配器模式、装饰器模式和代理模式的区别?
三者的代码实现非常相似,都用到了继承和组合,但在实现细节和目的上有着很大不同。
(1) 适配器模式:主要关心接口的转换,在不改变功能的情况下修改某个对象的接口。
(2) 装饰器模式:主要关心功能的扩展,在不改变接口的情况下增加某个对象的功能。
(3) 代理模式:主要关心对象的控制,它既不改变接口也不增加功能,它是对象间交流的中介,起到保护对象安全,控制对象行为的作用。
行为型模式
1. 什么是模版方法模式(Template)?
模板方法模式就是在基类中定义一个算法的框架,允许子类在不修改结构的情况下重写算法的特定步骤,这样子类就可以在基于父类的架构上有着自己独特的实现。
2. 如何实现模版方法模式(Template)?
创建一个模版抽象基类,在其中定义一个具体的公共算法框架,以及多个纯虚函数作为框架中的算法步骤,然后在各个子类中去重写这些虚函数,以实现自己的独有功能。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
3. 模版方法模式(Template)的优缺点?
(1) 优点:代码复用,去除了子类中的重复代码,仅需将某个具体步骤交由子类实现。
(2) 缺点:扩展性差,受到模版的约束,在子类中无法增加或删除某个算法步骤。
4. 什么是策略模式(Strategy)?
策略模式定义了一系列的算法,并且将每种算法都放入独立的类中,在实际使用时这些算法对象可以相互替换,将算法决定权交给使用者而不是编写者。
5. 如何实现策略模式(Strategy)?
策略模式定义了策略抽象基类,它的子类负责定义具体算法,这些算法是相互独立的。功能类会通过组合的方式去使用这些算法,但具体要使用哪个算法则由用户决定。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
6. 策略模式(Strategy)的优缺点?
(1) 优点:算法可以由用户动态切换,避免多重条件判断,扩展性良好
(2) 缺点:会增加较多的类,且所有的策略类都需要对外暴露才可以让用户理解与使用。
7. 什么是观察者模式(Observer)?
观察者模式在对象之间定义一对多的依赖,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并被自动更新,所以也叫“发布-订阅模式”。
8. 如何实现观察者模式(Observer)?
观察者模式由观察者类和主题类(被观察者类)组成。主题类给所有想观察它的观察者们提供了注册、注销以及同步数据的操作,而观察者则提供了一个数据更新操作。当主题类对象发生变化时,就可以通过同步数据使得所有观察者完成数据更新。
此处仅展示部分伪代码,在笔记末尾的附件中有完整代码和相关的测试用例
1 |
|
9. MFC中为什么有时候菜单项会是灰色的?
(1) 优点:观察者和被观察者是相对独立的,两者通过建立某种触发机制来完成数据同步。
(2) 缺点:如果观察者和被观察者之间有循环依赖,则观察行为会使两者进入死循环。观察者只能知道目标发生了某种变化,而不知道引发变化的原因。
更新记录
更新日期 | 更新详情 |
---|---|
(2023年07月01日) | 归纳总结了单例模式、工厂模式、适配器模式、装饰器模式、外观模式、代理模式、模版方法、策略模式、观察者模式等常见设计模式,并给出了相关的代码示例。 |
(2023年08月31日) | 将各个大板块重新编号,现在各个版块都有独立的题目编号,互不干扰。将更新记录中的版本号替换为年份日期,取消版本号机制,方便记录更新。 |
(2023年09月17日) | 修改首页文字布局,统一化布局。修改前言。添加前提基础模块。更改正文和标题字体。所有的更新日期都添加前置0,统一长度。将笔记改名为“设计模式(C++)”,从C++模块独立出来,仅标注为以C++实现。 |
附件
- 链接:https://pan.baidu.com/s/175TZK4HSWEJzrsG8rjG_wQ
- 提取码:nf3w
参考资料
《Head First设计模式》
《什么是设计模式》:https://blog.csdn.net/yi_chengyu/article/details/120427403
《设计模式:设计模式的七大原则》:https://blog.csdn.net/u010972055/article/details/106616376
《C++设计模式(全23种)》:https://blog.csdn.net/weixin_45712636/article/details/124328504
《设计模式》:https://subingwen.cn/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/
《12种设计模式C++源代码》:https://blog.csdn.net/qq_31052401/article/details/103741867