1、参考引用
- C++高级编程(第4版,C++17标准)马克·葛瑞格尔
2、建议先看《21天学通C++》 这本书入门,笔记链接如下
- 21天学通C++读书笔记(文章链接汇总)
1. 过程化的思考方式
- 过程语言(例如 C)将代码分割为小块,每个小块(理论上)完成单一的任务。如果在 C 中没有过程,所有代码都会集中在 main() 中,代码将难以阅读
- 计算机并不关心代码是位于 main() 中还是被分割成具有描述性名称和注释的小块。过程是一种抽象,它的存在是为了帮助程序员和阅读或维护代码的人。这个概念建立在一个与程序相关的基本问题之上:程序的作用是什么?用语言回答这个问题,就是过程化思考
2. 面向对象思想
- 与基于 “程序做什么” 问题的面向过程方法不同,面向对象方法提出另一个问题:模拟哪些实际对象?OOP(Object Oriented Programming)的基本观念是不应该将程序分割为若于任务,而是将其分为自然对象的模型。乍看上去这有些抽象,但用类组件、属性和行为等术语考虑实际对象时,这一思想就会变得更清晰
2.1 类
- 类将对象与其定义区分开来
- 类只是封装了用来定义对象分类的信息
- 所有对象都属于某个特定的类,对象是类的一个实例
2.2 组件
- 本质上,组件与类相似,但组件更小、更具体
- 如果考虑一个复杂的实际对象,可以看到它是由许多小组件组成
2.3 属性
- 属性将一个对象与其他对象区分开来
- 类属性由所有的类成员共享,而类的所有对象都有对象属性,但具有不同的值
- 属性用来描述对象的特征,回答 “为什么这个对象与众不同” 的问题
2.4 行为
- 行为回答两个问题:“对象做什么” 和 “能对对象做什么”
- 在面向对象编程中,许多功能性的代码从过程转移到类。通过建立具有某些行为的对象并定义对象的交互方式,OOP 以更丰富的机制将代码和代码操作的数据联系起来
- 类的行为由类方法实现
3. 对象之间的关系
3.1 “有一个(has a)”关系
- “有一个” 关系或聚合关系的模式是 A 有一个 B,或者 A 包含一个 B。在此类关系中,可认为某个对象是另一个对象的一部分。前面定义的组件通常代表 “有一个” 关系,因为组件表示组成其他对象的对象
3.2 “是一个(is a)”关系(继承)
- “是一个” 关系是面向对象编程中非常基本的概念,因此有许多名称,包括派生、子类、扩展和继承。类模拟了现实世界包含具有属性和行为的对象这一事实,继承模拟了这些对象通常以层次方式来组织这一事实。“是一个” 说明了这种层次关系。基本上,继承的模式是:A 是一个 B,或者 A 实际上与 B 非常相似
- 可定义 Animal 类,用以封装所有动物都有的属性 (大小、生活区域、食物等) 和行为 (走动、进食、睡觉)。特定的动物 (例如猴子) 成为 Animal 的子类,因为猴子包含动物的所有特征,它还有与众不同的其他特征
- 当类之间具有 “是一个” 关系时,目标之一就是将通用功能放入基类(base class),其他类可扩展基类。如果所有子类都有相似或完全相同的代码,就应该考虑将一些代码或全部代码放入基类。这样,可在一个地方完成所需的改动,将来的子类可 “免费” 获取这些共享的功能
3.2.1 继承技术
- 添加功能
- 派生类可在基类的基础上添加功能。例如,猴子是一种可挂在树上的动物。除了具有动物的所有行为以外,猴子还具有在树间移动的行为,即 Monkey 类有 swingFromTrees() 方法,这个行为只存在于 Monkey 类中
- 替换功能
- 派生类可完全替换或重写父类的行为。例如,大多数动物都步行,因此 Animal 类可能拥有模拟步行的 move 行为。但袋鼠是一种通过跳跃而不是步行移动的动物,Animal 基类的其他属性和行为仍然适用,Kangaroo 派生类只需要改变 move 行为的运行方式
当然,如果对基类的所有功能都进行替换,就可能意味着采用继承的方式根本就不正确,除非基类是一个抽象基类:抽象基类会强制每个子类实现未在抽象基类中实现的所有方法,无法为抽象基类创建实例
- 添加属性
- 除了从基类继承属性外,派生类还可添加新属性。企鹅具有动物的所有属性,此外还有 beak size (鸟喙大小) 属性
- 替换属性
- 与重写方法类似,C++ 提供了重写属性的方法。然而,这么做通常是不合适的,因为这会隐藏基类的属性,例如,基类可为具有特定名称的属性指定一个值,而派生类可给该属性指定另一个值
3.2.2 多态性与代码重用
- 多态性指具有标准属性和方法的对象可互换使用
- 在模拟动物园时,可通过编程遍历动物园中的所有动物,让每个动物都移动一次。由于所有动物都是 Animal 类的成员,因此它们都知道如何移动。某些动物重写了移动行为,但这正是亮点所在:代码只是告诉每个动物移动,而不知道也不关心是哪种动物,所有动物都按自己的方式移动
- 除多态性外,使用继承还有一个原因,通常这只是为了利用现有的代码。例如,如果需要一个具有回声效果的音乐播放类,而同事已经编写了一个播放音乐的类,但没有其他任何效果,此时可扩展这个已有的类,添加回声新功能
3.3 not-a 关系
- 当考虑类之间的关系时,应该考虑类之间是否真的存在关系。当实际事物之间存在明显关系,而代码中没有实际关系时,问题就出现了。OO (面向对象) 层次结构需要模拟功能关系,而不是人为制造关系。下图显示的关系作为概念集或层次结构是有意义的,但在代码中并不能代表有意义的关系
避免不必要继承的最好方法是首先给出大概的设计。为每个类和派生类写出计划设置的属性和行为。如果发现某个类没有自己特定的属性或方法,或者某个类的所有属性和方法都被派生类重写,只要这个类不是前面提到的抽象基类,就应该重新考虑设计
3.4 层次结构
- 正如类 A 可以是类 B 的基类一样,B 也可以是 C 的基类,面向对象层次结构可模拟类似的多层关系。当编写每个派生类的代码时,许多代码可能是相似的。此时应该考虑让它们拥有共同的父类
- 优秀的面向对象层次结构能做到以下几点
- 使类之间存在有意义的功能关系
- 将共同的功能放入基类,从而支持代码重用
- 避免子类过多地重写父类的功能,除非父类是一个抽象类
3.5 多重继承
- 在多重继承中,一个类可以有多个基类(通常应避免使用多重继承)
4. 抽象
4.1 接口与实现
- 抽象的关键在于有效分离接口与实现
- 实现是用来完成任务的代码,接口是其他用户使用代码的方式
- 在 C 中,描述库函数的头文件是接口,在面向对象编程中,类的接口是公有属性和方法的集合
- 优秀的接口只包含公有行为,类的属性/变量绝不应该公有,但可通过公有方法公开,这些方法称为获取器和设置器
4.2 决定公开的接口
- 当设计类时,其他程序员如何与你的对象交互是一个问题。在 C++ 中,类的属性和方法可以是公有的 (public)、受保护的 (protected) 和私有的 (private)
- public 意味着其他代码可以访问它们
- protected 意味着其他代码不能访问这个属性或行为,但子类可以访问
- private 意味着不仅其他代码不能访问这个属性或行为,子类也不能访问
- 设计公开的接口就是选择哪些应该成为 public
- 考虑用户:个人、团队中的其他成员、用户等
- 考虑目的:API、工具类或库、子系统接口、组件接口
- 考虑将来:如果将来的用途不明,就不要设计包含一切的日志类,因为这样做会不必要地将设计、实现和公有接口复杂化
5. 如何设计可重用代码
- 可重用代码有两个主要目标:代码必须通用、代码必须易用
5.1 使用抽象
- 使用抽象对于自己和使用代码的客户都有好处
- 客户会获得好处,因为他们不需要担心实现细节:他们可利用提供的功能,而不必理解代码的实际运行方式
- 你会获得好处,是因为可修改底层的代码,而不需要改变接口。这样就可升级和修订代码,而不需要客户改变他们的用法(如果使用动态链接库,客户甚至不需要重新生成可执行程序)
- 有时为将某个接口返回的信息传递给其他接口,库要求客户代码保存这些信息。这一信息有时叫作句柄 (handle),经常用来跟踪某些特定的实例,这些实例调用时的状态需要被记住
- 如果库的设计需要句柄,不要公开句柄的内部情况。可将句柄放入某个不透明类,程序员不能直接访问这个类的内部数据成员,也不能通过公有的获取器或设置器来访问
- 不要要求客户代码改变句柄内部的变量。一个不良设计的示例是,一个库为了启用错误日志,要求设置某个结构的特定成员,而这个结构所在的句柄本来应该是不透明的
5.2 构建理想的重用代码
5.2.1 避免组合不相干的概念或者逻辑上独立的概念
-
当设计组件时,应该关注单个任务或一组任务,即 “高聚合”,也称为 SRP(Single Responsibility Principle,单一责任原则)。不要将无关概念组合在一起,例如随机数生成器和 XML解析器
- 这个编程策略模拟了现实中可互换的独立部分的设计原则。例如,可编写一个 Car 类,在其中放入引擎的所有属性和行为。但引擎是独立组件,未与小汽车的其他部分绑定。可将引擎从一辆小汽车卸下,安装在另一辆小汽车中。合理的设计是添加一个 Engine 类,其中包含与引擎相关的所有功能。此后,Car 实例将包含 Engine 实例
-
将程序分为逻辑子系统
- 将子系统设计为可单独重用的分立组件,即“低耦合”。例如,如果设计一款网络游戏,应该将网络和图形用户界面放在独立的子系统中,这样就可以重用一个组件,而不会涉及另一个组件。假定现在要编写一款单机游戏,就可以重用图形界面子系统,但是不需要网络功能。与此类似,可以设计一个对等文件共享程序,在此情况下可重用网络子系统,但是不需要图形用户界面功能
-
用类层次结构分离逻辑概念
- 除了将程序分为逻辑子系统以外,在类级别上应该避免将无关概念组合在一起。例如,假定要为自驾车编写类。你决定首先编写小汽车的基本类,然后在其中直接加入所有自驾逻辑。但是,如果程序中只需要非自驾车,该怎么办?此时,与自驾相关的所有逻辑都会失效,而程序必须与本可避开的库 (如 vision 库和 LIDAR 库) 链接,解决方案是创建一个类层次结构,将自驾车作为普通汽车的一个派生类
- 用聚合分离逻辑概念
- 当不适合使用继承方法时,可以使用聚合分离没有关系的功能或者有关系但独立的功能。例如,假定要编写一个 Family 类来存储家庭成员。显然,树数据结构是存储这些信息的理想结构。不应该把树数据结构的代码整合到 Family 类中,而是应该编写一个单独的 Tree 类,然后 Family 类可以包含并使用 Tree 实例
5.2.2 对泛型数据结构和算法使用模板
-
C++ 模板的概念允许以类型或类的形式创建泛型结构。例如,假定为整型数组编写了代码。如果以后要使用 double 数组,就需要重写并复制所有代码。模板的概念将类型变成一个需要指定的参数,这样就可以创建个适用于任何类型的代码体。模板允许编写适用于任何类型的数据结构和算法
- 最简单的示例是 std::vector 类,这个类是 C++ 标准库的一部分。为创建整型的vector,可编写 std::vector<int>;为创建 double 类型的 vector,可编写 std::vector<double>
-
模板的问题
- 模板并不是完美的。首先,其语法令人迷惑,对于没有用过模板的人而言更是如此。其次,模板要求相同类型的数据结构,在一个结构中只能存储相同类型的对象
-
模板与继承
- 如果打算为不同的类型提供相同的功能,则使用模板。例如,如果要编写一个适用于任何类型的泛型排序算法,应该使用模板
- 如果要创建一个可以存储任何类型的容器,应该使用模板,关键的概念在于模板化的结构或算法会以相同方式处理所有类型。但是,如有必要,可给特定的类型特殊化模板,以区别对待这些类型
- 当需要提供相关类型的不同行为时,应该使用继承。例如,如果要提供两个不同但类似的容器,例如队列和优先队列,应该使用继承。
- 现在可以把二者结合起来,可以编写一个模板基类,此后从中派生一个模板化的类
-
扩展性
- 设计的类应当具有扩展性,可通过从这些类派生其他类来扩展它们。不过,设计好的类应当不再修改,也就是说,其行为应当是可扩展的,而不必修改其实现。这称为开放/关闭原则 (Open/ClosedPrinciple,OCP)
5.3 设计有用的接口
5.3.1 设计容易使用的接口
-
采用熟悉的处理方式
- 开发易于使用的接口的最佳策略是遵循标准的、熟悉的做事方法。当人们遇到的接口与他们过去用过的接口类似时,就能更好地理解这个接口,更容易采用这个接口,也不大可能用错。创新当然很重要,但应该在底层实现中创新,而不是在接口上
- 回到 C++,这个策略表明,开发的接口应该遵循 C++ 程序员熟悉的标准
- C++ 提供了一种叫作运算符重载的语言特性,帮助为对象开发易于使用的接口
-
不要省略必须的功能
- 首选,接口应该包括用户可能用到的所有行为
- 其次,在实现中包含尽可能多的功能
-
提供整洁的接口
- 不要在接口中提供多余的功能,保持接口的简洁
-
提供文档和注释
- 提供接口文档有两种方法:接口自身内部的注释和外部的文档。应该尽量提供这两种文档。大多数公开的 API 只提供外部文档:许多标准 UNIX 和 Windows 头文件中都缺少注释。在 UNIX 中,文档形式通常是名为 manpages 的在线手册。在 Windows 中,集成开发环境通常附带文档
5.3.2 设计通用接口
-
提供执行相同功能的多种方法
- 为让所有“顾客”都满意,有时可提供执行相同功能的多种方法。然而,应该慎用这种方法,因为过多的应用很容易让接口变得混乱不堪
-
提供定制
- 为增强接口的灵活性,可提供定制。定制可以很简单,如允许用户打开或关闭错误日志。定制的基本前提是向每个客户提供相同的基本功能,但给予用户稍加调整的能力
- 通过函数指针和模板参数,可提供更强的定制
5.3.3 协调通用性和使用性
-
提供多个接口
- 为在提供足够功能的同时降低复杂性,可提供两个独立接口。这称为接口隔离原则 (Interface SegregationPrinciple,ISP)。例如,编写的通用网络库可以具有两个独立的方向:一个为游戏提供网络接口,另一个为超文本传输协议 (HTTP,一种网络浏览协议)提供网络接口
-
让常用功能易于使用
- 当提供通用接口时,有些功能的使用频率会高于其他功能。应该让常用功能易于使用,同时仍提供高级功能选项