1、参考引用
- C++高级编程(第4版,C++17标准)马克·葛瑞格尔
2、建议先看《21天学通C++》 这本书入门,笔记链接如下
- 21天学通C++读书笔记(文章链接汇总)
1. 程序设计概述
- 在启动新程序(或已有程序的新功能)时,第一步是分析需求
- 分析阶段的重要输出是 “功能需求” 文档:描述新代码段到底要做什么,但它不解释如何做
- 需求分析后,也可能得到 “非功能需求” 文档:其中描述最终系统是什么,而非做什么。非功能需求的例子有:系统必须是安全的、可扩展的,还能满足特定性能标准等
- 第二步启动项目的设计阶段。程序设计(或软件设计)是为了满足程序的所有功能和非功能需求而实现的结构规范
- 大多数设计文档主要包括两个部分
- (1) 将总的程序分为子系统,包括子系统之间的界面和依赖关系、子系统之间的数据流、每个子系统的输入输出和通用线程模型
- (2) 每个子系统的详情,包括类的细分、类的层次结构、数据结构、算法、具体的线程模型和错误处理的细节
设计文档通常包括图和表格,以显示子系统交互关系和类层次关系
2. 程序设计的重要性
- 在编写代码之前进行规范的设计,有助于判断如何将所有内容组合在一起。程序的设计显示了程序子系统之间的关系和配合方式,以满足软件的需求。如果没有设计规划,可能会漏掉子系统之间的联系、重用或共享信息,以及失去用最简单方法完成任务的可能性。如果没有 “设计宏图”,可能会在某个实现细节上钻牛角尖,以至于忘记整体结构和目标
3. C++ 设计的两个原则
3.1 抽象
- 关键在于将接口与实现分开
- 抽象的作用
- 可使用代码而不必了解底层的实现。在此有一个简单示例,程序可调用在 <cmath> 头文件中声明的 sqrt() 函数,而不需要知道这个函数使用什么算法求平方根。实际上,平方根计算的底层实现可能因库版本而异:但只要接口不变,函数调用就可以照常运行
- 抽象原则也可以扩展到类:可使用 ostream 类的 cout 对象将数据传输到标准输出,不需要知道 cout 如何将文本输出到屏幕,只需了解公有接口
- 在设计中使用抽象
- 应该设计函数和类,使自己和其他程序员可以使用它们,而不需要知道(或依赖)底层的实现
3.2 重用
- 重用的设计思想适用于自己编写和使用的代码,应该设计程序,以重用类、算法和数据结构
- 在 C++ 中,模板是一种编写多用途代码的语言技术
// 可用于任何二维棋盘游戏的 泛型游戏棋盘类 template <typename PieceType> class GameBoard { public:void setPieceAt(size_t x, size_t y, PieceType *piece);PieceType *getPieceAt(size_t x, size_t y);bool isEmpty(size_t x, size_t y) const; private:... }
4. 重用代码
4.1 关于术语的说明
-
有三种可重用的代码
- 过去编写的代码
- 同事编写的代码
- 当前组织或公司以外的第三方编写的代码
-
所使用的代码可通过以下几种方式来构建
- 独立的函数或类:当重用自己或同事的代码时,通常会遇到这种类型
- 库:库是用于完成特定任务 (例如解析 XML) 或者针对特定领域 (如密码系统) 的代码集合。在库中经常可以找到其他许多功能,如线程和同步支持、网络和图像
- 框架:框架是代码的集合,围绕框架设计程序。例如,微软基础类 (Microsoft FoundationClasses,MFC) 提供了在 Microsoft Windows 中创建图形用户界面应用程序的框架。框架通常指定了程序的结构
-
应用程序编程接口(API)是另一个经常出现的术语。API 是库或代码为特定目的提供的接口。例如,程序员经常会提到套接字 API,这指的是套接字联网库的公开接口,而不是库本身
-
如果想要用 C++ 在 Microsot Windows 上编写图形用户界面 (GUI),应该使用 MFC(Microsoft Foundation Class) 或 Qt 等框架。你可能不知道如何在 Windows 上编写创建 GUI 的底层代码,更重要的是不想浪费时间去学习
4.2 重用代码的策略
4.2.1 理解功能和限制因素
- 对于多线程程序而言,代码安全吗?
- 库是否要求使用它的代码进行特定的编译器设置?如有必要,项目可以接受吗?
- 库或框架需要什么样的初始化调用?需要什么样的清理?
- 库或框架依赖于其他哪些库?
- 如果从某个类继承,应该调用哪个构造函数?应该重写哪些虚方法?
- 如果某个调用返回内存指针,调用者还是库负责内存的释放?如果库对此负责,什么时候释放内存?
- 强烈建议查看是否可使用智能指针来管理由库分配的内存。智能指针在第 1章中讨论过。
- 库调用检查哪些错误情况?此时做出了什么假定?如何处理错误?如何提醒客户端程序发生了错误?
- 应该避免使用弹出消息框、将消息传递到stderr/cerr 或stdout/cout 以及终止程序的库。
- 某个调用的全部返回值(按值或按引用)有哪些?
- 所有可能抛出的异常有哪些?
4.2.2 大 O 表示法
- 通常用大 O 表示法记录算法和库的性能
- 大 O 表示法表示相对性能而不是绝对性能。例如,大 O 表示法不会指出某个算法运行需要的时间,而是指出当输入量增加时算法如何执行。大 O 表示法仅适用于速度依赖于输入的算法,不适用于没有输入或者运行时间随机的算法
- 大 O 表示法用 O(n) 表示这个排序算法的性能。意味着使用大 O 表示法,n 表示输入量。O(n) 表示排序算法的速度是输入量的直接线性函数,并非所有算法的性能与输入量的关系都是线性的,下表按照性能好坏排序
4.2.3 理解性能的几点提示
- 当数据量加倍时,算法所需要的时间也加倍,这根本就没有说需要多长时间!如果某个糟糕的算法具有较大的规模,这绝不符合需求。例如,如果算法进行了不必要的磁盘访问,可能不会影响大 O 表示法,但性能非常糟糕
- 按照这一思路,很难比较两个具有相同大 O 运行时间的算法。例如,两个不同的排序算法都声称 O ( n l o g n ) O(nlogn) O(nlogn),如果不进行测试,很难说哪个算法实际上更快些
- 大 O 表示法描述了算法的渐进时间复杂度,因为输入量会无限增大。对于小规模输入,大 O 时间很容易引起误解:当输入量规模不大时, O ( n 2 ) O(n^2) O(n2) 算法的实际执行性能可能要优于 O ( l o g n ) O(logn) O(logn) 算法
4.2.4 理解平台限制
- 在开始使用库代码之前,一定要理解运行库的平台。这看上去是显而易见的,但即使是那些号称跨平台的库,在不同的平台上也会有微妙差别。此外,平台不仅包括不同的操作系统,还包括同一操作系统的不同版本
4.2.5 理解许可证和支持
- 使用第三方的库常会带来复杂的许可证问题。为使用第三方供应商提供的库,有时必须支付许可证费用。还可能有其他的许可限制,包括出口限制。此外,开放源代码库有时会要求与其有关的任何代码都公开源代码
- 使用第三方库还带来了支持问题。在使用某个库之前,一定要理解提交 bug 的过程,并了解修正 bug 所需的时间。如果可能,判断这个库会被支持多长时间,这样就可以相应地制定计划
4.2.6 原型
- 当首次使用某个新库或框架时,最好编写一个快速原型。测试代码是熟悉库功能的最好方法。应该考虑在程序设计之前测试库,这样就可以熟悉库的功能和限制。这种实际检验还可判断库的性能特征
- 即使原型应用程序与最终应用程序没有任何相似之处,花费在原型上的时间也不会浪费。不要觉得编写实际应用程序的原型很难,可编写一个虚拟程序来测试想使用的库功能,这样做是为了让自己熟悉库
5. 设计一个国际象棋程序
5.1 需求
- 在开始设计前,应该弄清楚对于程序功能和性能的需求。理想情况下,这些需求应该是以需求规范 (requirements specification) 形式给出的文档。国际象棋程序的需求应该包含下列类型的规范,当然实际的需求规范应该比下面的内容更详细,条目更多
- 程序支持标准的国际象棋规则
- 程序支持两个玩家。程序不提供具有人工智能的计算机玩家
- 程序提供基于文本的界面
- 程序以纯文本形式提供棋盘和棋子
- 玩家通过输入代表位置的数字在棋盘上移动棋子
5.2 设计步骤
在需要时设计应该包含图示和表格,制作图示的行业标准称为 UML(统一建模语言),UML 定义了一组标准图示,可用于说明软件设计 (如类图、序列图等)。建议使用 UML,至少也要尽量使用类似 UML 的图示。但不一定要严格遵循 UML 语法,因为图示清晰、易于理解,要比语法正确更重要
5.2.1 将程序分割为子系统
- 设计的第一步是将程序分割为通用功能子系统,并指明子系统之间的接口和交互关系。此时不需要考虑特定的数据结构和算法,甚至不需要考虑类,只是试着感受程序不同部分和它们之间的交互关系
- 建议使用模型-视图-控制(MVC)模式将数据存储和数据显示明确分离
- MVC 模式建立了如下理念:许多应用程序经常要处理一组数据,处理这些数据上的一个或多个视图,并操作这些数据。在 MVC 中,这组数据称为模型,视图是模型的一个特定界面,控制器修改模型,以响应某个事件的代码。MVC 的 3 个组件在反馈循环中交互操作,动作由控制器处理,控制器会调整模型,把修改返回到视图中
- 在 MVC 设计中,下标中的 ChessBoard 和 ChessPiece 子系统是模型部分,ChessBoardView 和ChessPieceView 是视图部分,Player 是控制器部分
- 由于表格无法形象地表示子系统之间的关系,通常会使用图示来表明程序的子系统,在此箭头表示一个子系统对另一子系统的调用。下图用 UML 用例图显示了国际象棋游戏的各个子系统
5.2.2 选择线程模型
- 在设计阶段,可以选择程序中高级线程的数目并指定线程的交互方式
- 高级线程的示例有 UI 线程、音频播放线程、网络通信线程等
- 在多线程设计中,应该尽可能避免共享数据,这样可使程序更简单、更安全
- 如果无法避免共享数据,应该指定加锁需求。如果不熟悉或平台不支持多线程,那么程序应该是单线程
- 然而,如果程序有多个不同的任务,每个任务都并行运行,多线程是个不错的选择。例如:图形用户界面程序经常让一个线程执行主程序,其他线程等待用户按下按钮或者选择菜单项
- 国际象棋程序只需要一个线程来控制游戏流程
5.2.3 指定每个子系统的类层次结构
- 国际象棋程序需要一个类层次结构来代表棋子,这个类层次结构如下图所示。在这个类层次结构中,ChessPiece 泛型类作为抽象基类,ChessPieceView 类也有类似的类层次结构
- 另一个类层次结构用于 ChessBoardView 类,以实现游戏的文本界面或用户图形界面。下图给出了这个类层次结构,可以在控制台以文本方式显示棋盘,也可以用 2D 或 3D 图形显示棋盘,Player 控制器和 ChessPieceView 类层次结构的各个类也需要类似的类层次结构