C++ shorthand
重要
C++ 五步走
- 预处理
- 带
#
的都处理一通,#define 和 #include 都把内容给换进来,#if 判断下 - 输出:没有宏和预处理指令的纯C++代码文件
- 带
- 编译
- 语法分析(写对没),语义分析(用对没),生成中间代码并优化
- 输出:汇编代码
- 汇编 (Assembly)
- 汇编器把汇编代码转化成机器代码
- 输出:机器代码/目标文件(.o/.obj)
- 链接
- 链接器会将目标文件和库文件链接到一起,包括:
- 符号解析,地址重定位,库链接
- 输出:可执行文件(.out, .exe)
- 链接器会将目标文件和库文件链接到一起,包括:
- 运行,没啥好说的,os把它加载到内存,加载动态链接库然后执行
链接,****静态库 & 动态库
链接的步骤:
- 符号解析:符号就是变量、函数等等 entity 的名字,编译阶段他们是引用或者定义,而链接器会把他们解析为实际的地址,或者具体的实现
- 地址重定位:代码、数据、符号一开始都是用的相对地址,但是生成可执行文件时我们需要绝对地址。重定位有时还会调整一下代码和数据的位置
- 合并段:目标文件一般有好几个段,比如.text存代码,.data 存局部变量,链接器会把所有目标文件
- 库文件处理:分别处理静态库(.lib/.a),和动态库(.dll, .so)
- 静态库:直接把相关代码copy进exe里
- 动态库:记录库的引用,运行时os负责加载它(真链接)
智能指针(Smart Pointers)#include
;
C++ 11 标准,它是一个类模板,包装一个指针,在智能指针对象声明周期结束时自动释放动态内存
基本使用方法(以unique为例)
std::unique_ptr<T> ptr = make_unique<T>(initial value)
*就用auto吧ptr.reset()
提前销毁它(不这么做,则是离开作用域之后销毁它)
三种智能指针:
std::unique_ptr
独占- 用法:
unique_ptr<T> ptr1 = make_unique<T>(init_val);
- 思想:任何时刻,一个资源只能有一个指针拥有
- 特性:
- 禁止复制(不支持‘=’),但可以
std::move
移动
- 禁止复制(不支持‘=’),但可以
- 用法:
std::shared_ptr
共享- 用法:
shared_ptr<T> ptr1 = make_shared<T>(init_val);
- 最后一个共享指针销毁时,才会释放这块内存(通过引用计数实现,也就是引用计数归零时释放内存)
- 用法:
std::weak_ptr
弱引用(配合share使用)- 它是 一个由 shared_ptr 管理的对象的 弱引用,它不会增加引用计数,但也不能直接访问,而是需要调用 lock() 方法访问(lock方法会尝试获取指向该对象的shared_ptr, 如果对象无了,那就返回空)
- 用法:
weak_ptr<T> p = ptr;
(ptr 得是共享指针) - 思想:打破共享指针的
循环引用
- 循环引用:如果两个shared_ptr 互相持有,可能会出现好几个人的count都不为0的情况
shared_ptr 的 Reference Counting原理 & 为什么会出现循环引用无法销毁?
std::shared_ptr
使用引用计数来跟踪有多少个 shared_ptr
实例共享同一个对象。引用计数可以简单地理解为一个计数器,用来记录当前有多少个 shared_ptr
正在引用该对象
- 强引用计数(use count):记录当前有多少个
shared_ptr
实例指向同一个对象 - 弱引用计数(weak count):记录当前有多少个
weak_ptr
实例指向同一个对象。
使用make_shared 创建对象时,use count 设置为1,每次copy给其他ptr时use count + 1,回收的时候 use count - 1,然后weak count是独立的。
循环引用的关键是,两边都在等待对方先释放。设想下面的情景:
我们通过 pa 和 pb 申请了Obj A 和 Obj B,同时对象 A 里有一个 B 类型的ptr(也就是 pa->b_ptr),它也指向 Obj B,对象 B 亦然,显然 Obj A 和 B 的 count = 2。
s
此时 ptr_A 和 ptr_B 离开了作用域,因此它们都被销毁,按理来说此时 Obj A 和 Obj B 也应该被销毁,但这时 Obj A 和 Obj B 的 count 都是1,所以没办法触发回收。
解决方案:类似一个死锁,必须得有一边先释放count,才能把两个对象都销毁,这也是为什么 weak_ptr 可以解决这个循环引用问题
decltype & auto
- auto 在编译期就可以推导出变量的类型
- 限制:不能作为参数,不能定义数组
- decltype(declear type):推导出这个表达式的类型,但是不会执行它
decltype(a) b
=> 创建一个和 a 一样类型的对象,这点和auto 的用法差不多- 用在返回值 / 泛型编程很不错:
auto add(T1 a, T2 b) -> decltype(a + b)
杂项
- Is_A or Has_A?
- is-a 对应的是类的继承关系, has-a 则是类的组合关系
- 举例:cat is an animal, car has an engine
- cat 就是派生自 animal,engine则是作为car的成员实现组合
- inline(内联) 如果你有for循环,就别用了, 不会生效的
- 为什么用 ++i, 而不是 i++:i++ 需要创建一个临时对象(构造和析构而造成的格外开销 ),但是 ++i 不用
- 构造函数不能声明为虚函数,析构函数则应当声明为虚函数
- 必须明白,虚函数是通过指向派生类的基类指针/引用,来访问派生类的同名函数,所以让virtual关键字起效,一定是通过基类来访问子类的方法。
- 构造函数创建的时候马上调用,virtual 是不可能生效的
- 析构:假如你的基类指针指向了一个派生类对象,如果你不写成虚函数,那么释放这个指针的时候,它就只会调用基类的析构,那派生类的资源就清理不干净了,就内存泄漏了
- 为什么创建对象使用指针?而不是直接创建:
- 最重要的是:你直接创建那就是一个局部变量,离开作用域他就被销毁了,假如你在func里创建了一个变量,返回去的话,你这链表、二叉树不就断了吗
- 使用new创建指向这个对象的指针时,这个对象实际上会存在堆上,即使创建大量的对象也不容易爆内存(指stack overflow,普通创建的对象会被静态分配到stack区域,而stack区域比heap区域小很多)。
- 我可以随时回收它,同时对于有一些对象会涉及到一些变长的属性,创在heap上是可以改长度的
- CPP中动态与静态的联想:
- 动态 => 运行时确定、可修改(灵活,但消耗更多资源)、堆空间..
- 静态 => 编译时确定、不可修改(只读,性能好)、栈空间…
关键字
pointer & refference
- 指针是一种变量,指针的值是变量的地址
int* prt = &x
- 引用是变量的别名,必须依赖变量存在,必须初始化,不能改变绑定的对象
函数指针 & 指针函数
Function Pointer:这个指针指向的是一个函数,指向不同的位置就可以用同一个指针访问不同的函数,相当于一个手动多态?用法:
1 |
|
[返回值, (名字), (参数列表)]
Pointer to Function:返回指针的函数。
const & constexpr
const
- 作用:被它修饰的值不能改变。必须在定义的时候就给它赋初值
- 修饰成员函数(不能修饰普通函数):不能在函数中修改任何成员变量或调用非
const
成员函数 - 特殊处理:常量指针(底层const) & 指针常量(顶层const);口诀:左定值,右定向
- 常量指针 Pointer to a Constant:
const int* p = &val
- 不可以通过 p 去修改 val。但 p 可以指向另一个变量,也可以直接修改 val
- 用法:你准备将一个指针传入func,但你不希望这个函数去修改你传进去的变量本身
- 指针常量 Constant Pointer:
int* const p = &val
- 不可以修改 p 指向的变量,但可以通过 p 来修改 val。毕竟指针的值是地址。
- 常量指针 Pointer to a Constant:
constexpr
- 更严格的const,它的值在编译阶段就需要确定,可以和const一起用,可以用在函数上(const不行)用在函数上时这个函数在编译时就会计算。
- 简单来说,可以提供鲁棒性保障 + 编译优化?
static 共享数据 / 隐藏数据
- 静态局部变量
- 当
static
修饰局部变量时,该变量的生命周期扩展到程序的整个运行周期,而不仅仅限于它所在的函数或代码块。即使函数多次调用,static
局部变量也只会初始化一次,且其值在函数调用之间保持 - (离开这个作用域后当前的变量就不可见了,但回来之后你会发现这个变量还在,而且没有被销毁并重新创建,而是维持最后的value
- 当
- 静态类成员 (当前类成员变为类作用域下的全局变量)
- 当
static
修饰类成员(变量或方法)时,这个成员属于类而不是某个特定的对象。也就是说,所有类的实例共享这一成员。 - 通过
Aclass::val / Aclass::method()
可以直接访问,不需要实例化 - 对于变量来说,需要在类外声明并定义它,如
int Aclass::static_val = 0
,所有的对象共享的是同一个变量(副本) static
成员只能访问static
成员,不能访问别的
- 当
- 静态全局变量/函数
- 让当前的变量只在当前的文件可见,那么其他文件定义的同名变量就不会冲突了
剩下几个关键字
- extern :告诉编译器这个变量的定义 可能不在当前文件,对于func,默认会extern
- 声明外部变量,如你在
file1.cc
定义int a = 10;
, 在file2.cc
里定义extern int a;
编译器会去其他文件里找这个变量,并连接这两个文件的
- 声明外部变量,如你在
- volatile :确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问
- final: 放在声明的最后(和 override 一样) 表示这就是最终版本了。如果是
class A final{}
那 A 就不能被继承;如果是一个虚函数,那么我们就不能再继续override他了
内存管理
内存分区
这图得背
- stack内存自动管理。局部变量超出作用域会直接释放,过大的consume会产生 stack overflow
- heap 是你手动管理的,会出现memory leak,Valgrind 可以检测
- Global/Static Memory,程序整个周期都不释放
- Constant Memory,只读区域
静态分配,动态分配 「一般动态 => 运行时决定,静态 => 编译时决定」
- 动态分配:运行的时候才会分配,一你吃大小可以改变,分配的是heap的空间,也就是你用new的时候才需要操作heap,最后一定需要delete
- 静态分配:直接创建的变量、常量会被放在stack / static 区域,编译的时候就给你创建好,你运行的时候可以改,但它的大小是不会变的
内存泄漏
new & malloc,选new
- new是C++运算符,malloc是库函数(C)
- new是malloc的封装,new会调用构造和析构
- new会返回具体类型指针,是类型安全的
delete & free,选delete
- delete会调用析构函数,会把内存块指针置为nullptr,可以正确释放数组;这几个free都不行
Memory Leak 是什么, 怎么解决?
- 在程序运行时申请的动态内存,如果没有正确释放,就会产生泄漏,因为他们无法被重新分配。
- 发生泄漏的可能原因:1. new 了不delete;2. 用来接受 new 的指针被覆盖掉了,没办法再访问它;3. 循环引用(复杂数据结构之间相互引用)
- 怎么解决:
- 封装到class里,不要直接申请
- 智能指针
- 检测工具:
- Valgrind,ASan
内存对齐
struct 有关
数据结构的起始地址是特定的对齐边界的倍数
在结构体中,编译器为结构体的每个成员按 其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整 个结构体的地址相同。
对于一个空的结构体,C++会为他的 instance 分配 1 byte(空的class也是如此),确保每个实例都有唯一的内存地址。
内存对齐的核心:尽量让CPU少访问几次内存,
比如int,如果放在0x00000002 上面,那CPU就需要取两次了,放0x00000004一次就取走了
面向对象
- OOP explain:OOP 是一种程序设计范式,它基于一个个对象,来组织代码,一个对象是一个包含属性 和 行为的实例,
- why OOP:与之相对的是面向过程编程,使用OOP时问题的基本单元就变成了对象
- 好处:可重用性(继承)、模块化(封装)、和灵活性(多态)
OOP 三大特性:继承,封装,多态
继承 Inheritance
基本概念
- Base Class 父类:Base class 的私有成员无论哪种继承,都是不可访问的
- Derived Class 派生类:可以直接使用父类的属性和方法,并且可以添加新的属性和方法或重写(覆盖)父类的方法。
protected
:和 private 最大的区别就是,protected 派生类是可以访问的,但是 private 对象派生类是无法访问的
基本类型
- 公共继承
class Dervied : public Base
- 正常访问就行(私有的不能访问)
- 保护继承
class Derived : protected Base
- Base class 的所有成员都被视为保护成员,你可以写方法调用,但不能从外面访问了(不能利用对象来访问它)(
public
和protected
都变为protected
)
- Base class 的所有成员都被视为保护成员,你可以写方法调用,但不能从外面访问了(不能利用对象来访问它)(
- 私有继承
class Derived : private Base
- Base class 的所有成员都被视为 Derived class 的私有成员,也就是说,如果你是 Dervied class 的子类,那么你只能访问 Derived class 新定义的 public 和 protected 对象,而 Base class 里的所有对象都不能访问
特性
- 重载和覆盖(重写)(Overloading and Overriding)
「 静态多态」&「动态多态(在派生类中重新定义基类中已经定义的虚函数)」
「同名不同参数 & 虚函数(名和参数都要相同) 」
- 多重继承(Multiple Inheritance)& 虚继承(Virtual Inheritance)
- 一个类可以同时继承多个父类
- 菱形继承:D 继承了 C 和 B,然而 C 和 B 都是 A 的子类,这时在 C 和 B 的继承声明时 写成虚继承
class C : virtual public A
,可以避免在派生类里面生成多个A的实例
继承 / 派生的思路(结合设计模式理解)
- 单继承:表示 is-a 关系,最经典的例子:
class cat : public Animal
=> cat is animal - 抽象类与接口:我的基类包含一些
纯虚函数
,存在的意义就是定义出通用的接口,本身是不会实例化的,让子类去实现具体细节 - 模版方法模式:我先定义一个算法框架,然后另外一些步骤写成虚函数,让子类可以去实现自己的version
- 可以使用组合的方式(就是在一个class里去创建另一个class的对象),维护其实挺方便的
封装 Encapsulation
封装的主要体现就是利用访问控制修饰符
(public, private…),以及使用 getter & setter的思想
多态 Polymorphism
允许一个接口或方法可以表现为多种不同的形式。换句话说,多态使得不同类型的对象可以用统一的方式处理,而具体的行为则取决于对象的实际类型。
虚函数/纯虚函数 & 虚函数表(virtual table/vtable)
- 虚函数:基类函数声明时添加 virtual:
virtual void show()
;派生类里覆盖它:void show() override
。这时你的(指向派生类对象的)基类指针调用show函数时,会调用派生类里的函数,而不是基类里的。(指向哪个派生类对象就调用哪个派生类的虚函数)Base* basePtr = new Derived()
- virtual 体现在它是所谓的
动态联编/绑定
,它的调用是在运行的时候确定的,而不是编译时
- 纯虚函数:
virtual void show()=0
,这时编译器会要求子类必须override这个函数,同时,只要你的class声明了纯虚函数,它就会变成抽象类,不能创建它的实例,而只能由子类去继承并且实现你给出的接口。- 比如:animal : cat/dog 这组关系,创建一个animal 的 instance 是有点奇怪的
- 我在子类里实现纯虚函数之后,它就会变成一个普通虚函数,继续派生时子类可以override 它
- vtable
- 编译器在编译时生成的一个数组,存储类中所有虚函数的指针(如果你定义了虚函数,编译器就会为你创建vtable)
- 工作原理:对于有虚函数的类,在创建对象时会额外创建一个隐藏指针
vptr
指向这个类的vtable 和虚函数表(指针数组
- 用法:
- 基类指针直接指向派生类对象,然后通过这个指针直接调用派生类的函数
重载和覆盖(Overloading and Overriding)
- overload,静态多态:
- 同个field里允许多个名字一样的方法存在(前提是参数列表不一样)
- 运算符重载
- override,动态多态:
- 子类可以重新定义父类中的虚函数,运行时可以调用子类的版本 (
void functionName() override
)
- 子类可以重新定义父类中的虚函数,运行时可以调用子类的版本 (
多线程
1 |
|
STL
👷
设计模式
单例模式(创建型):
用于确保一个类只有一个实例,并提供一个全局访问点,在需要实例化的场景下使用
数据库连接池、日志管理等,避免一个全局访问的类频繁的创建和销毁,浪费资源
资源共享:当多个模块或系统需要共享某⼀资源时,可以使⽤单例模式确保该资源只被创建⼀次,避免重复创建和浪费资源。
控制资源访问:单例模式可以⽤于控制对特定资源的访问,例如数据库连接池、线程池等。
配置管理器:当整个应⽤程序需要共享⼀些配置信息时,可以使⽤单例模式将配置信息存储在单例类中,⽅便 全局访问和管理。
⽇志记录器:单例模式可以⽤于创建⼀个全局的⽇志记录器,⽤于记录系统中的⽇志信息。
线程池:在多线程环境下,使⽤单例模式管理线程池,确保线程池只被创建⼀次,提⾼线程池的利⽤率。
缓存:单例模式可以⽤于实现缓存系统,确保缓存只有⼀个实例,避免数据不⼀致性和内存浪费。
1 |
|
工厂模式(创建型):
当⼀个类不知道它所需要的类的时候。 当⼀个类希望通过其⼦类来指定创建对象的时候。 当类将创建对象的职责委托给多个帮助⼦类中的某⼀个,并且希望将哪⼀个帮助⼦类是代理者的信息局部化 时。
举例来说:
在数据库操作中,通过⼯⼚模式可以根据不同的数据库类型(MySQL、Oracle等)创建对应的数据库连接对 象
通过⼯⼚模式可以根据配置⽂件或其他条件选择不同类型的⽇志记录器,如⽂件⽇志记录器、数据库⽇志记录 器等。
在图形⽤户界⾯(GUI)库中,可以使⽤⼯⼚模式创建不同⻛格或主题的界⾯元素,如按钮、⽂本框等。
在加密算法库中,可以使⽤⼯⼚模式根据需要选择不同的加密算法,例如对称加密、⾮对称加密等。
在⽂件解析过程中,可以使⽤⼯⼚模式根据⽂件类型选择不同的解析器,如XML解析器、JSON解析器等。
在⽹络通信库中,可以使⽤⼯⼚模式创建不同类型的⽹络连接对象,如TCP连接、UDP连接等。
1 |
|
观察者模式(行为型):
它定义了⼀种⼀对多的依赖关系,让多个观察者对象同时监听某⼀个主题对象。这个主题 对象在状态变化时,会通知所有的观察者对象,使他们能够⾃动更新⾃⼰。
使⽤场景
- 事件处理:当⼀个对象的状态发⽣改变时,观察者模式可以⽤于通知和处理与该对象相关的事件。这在图形⽤ 户界⾯(GUI)开发中是很常⻅的,例如按钮点击事件、⿏标移动事件等。
- 发布订阅:观察者模式可以⽤于实现发布-订阅模型,其中⼀个主题(发布者)负责发送通知,⽽多个观察者 (订阅者)监听并响应这些通知。这种模型在消息队列系统、事件总线等场景中经常使⽤。
- MVC架构:观察者模式常被⽤于实现MVC架构中的模型和视图之间的通信。当模型的状态发⽣改变时,所有相关的视图都会得到通知并更新显示。
- 异步编程:观察者模式可以⽤于处理异步任务的完成事件。任务完成时,通知所有相关的观察者进⾏后续处理。
1 |
|
代理模式(结构型)
⽬的是在访问某个对象时引⼊⼀种代理对象,通过代理对象控制对原始对象的访问。代理模式可以⽤于实现控制对对象的访问、延迟加载、实现权限控制、监控对象等场景。
- 抽象主题( Subject ): 定义了代理对象和真实对象的共同接⼝,使得代理对象能够替代真实对象。
- 真实主题 Real Subject : 是实际执⾏业务逻辑的对象,是代理模式中的被代理对象。
- 代理 Proxy : 包含⼀个指向真实主题的引⽤,提供与真实主题相同的接⼝,可以控制对真实主题的访问,并在需要时负责创建或删除真实主题的实例。
1 |
|
在这个示例中,Subject
是抽象主题类,定义了请求的接口。RealSubject
是具体主题类,实现了真正的请求处理逻辑。Proxy
是代理类,代理了真实主题对象,可以在执行请求之前进行权限验证等操作。在 main
函数中,创建了代理对象 proxy
,并调用了它的 request
方法来处理请求。
策略模式(行为型)
定义了⼀系列算法,把它们单独封装起来,并且使它们可以互相替换,使得算法可以独⽴于使⽤它的客户端⽽变化(算法所完成的功能类型是⼀样的,对外接⼝也是⼀样的,只是不同的策略为引起环境⻆⾊表现出不同的⾏为。)
缺点:可能需要定义⼤量的策略类,并且这些策略类都要提供给客户端
策略模式使得算法可以独立于使用它的客户端变化
1 |
|
在这个示例中,Strategy
是抽象策略类,定义了一个执行策略的纯虚函数。ConcreteStrategyA
和 ConcreteStrategyB
是具体策略类,分别实现了不同的算法。Context
是上下文类,负责维护当前使用的策略对象,并在需要时执行相应的策略。
在 main
函数中,首先创建了一个上下文对象,并使用策略A初始化它,然后执行了策略A。接着,通过调用 setStrategy
方法切换至策略B,并执行了策略B。这样,客户端可以在运行时灵活地选择不同的策略来执行任务。
装饰模式(结构型)
它允许在不改变原始类接⼝的情况下,动态地添加功能或责任。灵活地扩展功能
装饰模式通过创建⼀个装饰类,包裹原始类的实例,并在保持原始类接⼝不变的情况下,提供额外的功 能。就增加功能来说,装饰模式⽐⽣成⼦类更为灵活
1 |
|
在这个示例中,Component
是抽象组件类,定义了一个操作函数。ConcreteComponent
是具体组件类,实现了实际的操作逻辑。Decorator
是抽象装饰器类,继承自 Component
,并持有一个指向组件的指针。ConcreteDecoratorA
和 ConcreteDecoratorB
是具体装饰器类,分别实现了不同的功能扩展。在 main
函数中,首先创建了一个具体组件对象,然后使用装饰器A包裹组件对象,再使用装饰器B包裹装饰器A,最后执行了操作。通过装饰器的嵌套组合,可以动态地扩展对象的功能。
适配器模式(结构型)
用于将一个类的接口转换成客户端所期望的另一个接口。这种模式通常用于旧接口与新接口不兼容的情况下,通过适配器将旧接口适配成新接口,使得客户端可以无需修改现有代码而直接使用旧接口。
1 |
|
在这个示例中,RoundHole
和 RoundPeg
分别是目标接口,代表了圆孔和圆钉。SquarePeg
是需要被适配的类,代表了方钉。SquarePegAdapter
是适配器类,继承自 RoundPeg
,通过适配器,方钉可以被当作圆钉来使用。在 main
函数中,首先创建了一个圆孔和一个圆钉,然后创建了一个方钉并通过适配器将其适配成了圆钉,最后分别测试了圆钉和适配后的方钉是否能插入圆孔。
创建型模式
1、单例(singleton)模式: 保证⼀个类只有⼀个实例,并提供⼀个访问它的全局访问点
2、工厂方法(factory method)模式: 定义⼀个创建对象的接⼝,但由⼦类决定需要实例化哪⼀个类。⼯⼚⽅法使得⼦类实例化的过程推迟
3、抽象⼯⼚(abstract factory)模式: 提供⼀个接⼝,可以创建⼀系列相关或相互依赖的对象,⽽⽆需指定他们具体的类
4、原型(prototype)模式: ⽤原型实例指定创建对象的类型,并且通过拷⻉这个原型来创建新的对象
5、构建器(builder)模式: 将⼀个复杂类的表示与其构造相分离,使得相同的构建过程能够得出不同的表示
结构型模式
1、代理(proxy)模式: 为其他对象提供⼀种代理以控制这个对象的访问
2、装饰(decorator)模式: 动态地给⼀个对象添加⼀些额外的职责。它提供了⽤⼦类扩展功能的⼀个灵活的替代,⽐派⽣⼀个⼦类更加灵活 ——速记关键字:附加职责
3、适配器(adapter)模式: 将⼀个类的接⼝转换成⽤户希望得到的另⼀个接⼝。它使原本不相容的接⼝得以协同⼯作——速记关键字:转换接⼝
2、桥接(bridge)模式: 将类的抽象部分和它的实现部分分离开来,使它们可以独⽴地变化——速记关键字:继承树拆分
3、组合(composite)模式: 将对象组合成树型结构以表示“整体-部分”的层次结构,使得⽤户对单个对象和组合对象的使⽤具有⼀致性——速记 关键字:树形⽬录结构
5、外观(facade)模式: 定义⼀个⾼层接⼝,为⼦系统中的⼀组接⼝提供⼀个⼀致的外观,从⽽简化了该⼦系统的使⽤——速记关键字:对 外统⼀接⼝
6、享元(flyweight)模式: 提供⽀持⼤量细粒度对象共享的有效⽅法
示
行为型模式
1、观察者(observer)模式: 定义对象间的⼀种⼀对多的依赖关系,当⼀个对象的状态发⽣改变时,所有依赖于它的对象都得到通知并⾃动更新
2、策略(strategy)模式: 定义⼀系列算法,把它们⼀个个封装起来,并且使它们之间可互相替换,从⽽让算法可以独⽴于使⽤它的⽤户⽽变 化
3、职责链(chain of responsibility)模式: 通过给多个对象处理请求的机会,减少请求的发送者与接收者之间的耦合。将接收对象链接起来,在链中传递请 求,直到有⼀个对象处理这个请求——速记关键字:传递职责
4、命令(command)模式: 将⼀个请求封装为⼀个对象,从⽽可⽤不同的请求对客户进⾏参数化,将请求排队或记录请求⽇志,⽀持可撤销的 操作——速记关键字:⽇志记录,可撤销
5、解释器(interpreter)模式: 给定⼀种语⾔,定义它的⽂法表示,并定义⼀个解释器,该解释器⽤来根据⽂法表示来解释语⾔中的句⼦
6、迭代器(iterator)模式: 提供⼀种⽅法来顺序访问⼀个聚合对象中的各个元素⽽不需要暴露该对象的内部表示
7、中介者(mediator)模式: ⽤⼀个中介对象来封装⼀系列的对象交互。它使各对象不需要显式地相互调⽤,从⽽达到低耦合,还可以独⽴地改 变对象间的交互——速记关键字:不直接引⽤
8、备忘录(memento)模式: 在不破坏封装性的前提下,捕获⼀个对象的内部状态,并在该对象之外保存这个状态,从⽽可⽤在以后将该对象恢 复到原先保存的状态
9、状态(state)模式: 允许⼀个对象在其内部状态改变时改变它的⾏为——速记关键字:状态变成类
10、模板⽅法(template method)模式: 定义⼀个操作中的算法⻣架,⽽将⼀些步骤延迟到⼦类中,使得⼦类可以不改变⼀个算法的结构即可重新定义算法 的某些特定步骤
11、访问者(visitor)模式: 表示⼀个作⽤于某对象结构中的各元素的操作,使得在不改变各元素的类的前提下定义作⽤于这些元素的新操作
设计原则
单⼀职责 在设计类的时候要尽量缩⼩粒度,使功能明确、单⼀,不要做多余的事情(⾼内聚,低耦合)
开闭原则 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着可以通过扩展来添加新功能,⽽不必修改现有代码。
⾥⽒替换 ⼦类型必须能够替换掉它们的基类型。在程序中,如果有⼀个基类和⼀个⼦类,那么可以⽤⼦类对象替换基类对象,⽽程序的⾏为仍然是正确的。
接⼝隔离 不应该强迫⼀个类实现它不需要的接⼝。⼀个类不应该对它⽤不到的⽅法负责。
依赖倒置 ⾼层模块不应该依赖于低层模块,⽽是应该依赖于抽象。抽象不应该依赖于具体实现,具体实现应该依赖于抽象。 ⽐如Java的操作数据库,Java定义了⼀组接⼝,由各个数据库去实现它,Java不依赖于他们,数据库依赖于Java
组合优于继承 继承耦合度⾼,组合耦合度低 继承基类是固定好的,但是组合通过组合类的指针,可以传⼊不同的类,避免⾼耦合