【C++高级编程】(二)设计专业的C++程序

 本章内容:

  • 程序设计的定义
  • 程序设计的重要性
  • C++程序设计的特点
  • 高效C++程序设计的两个基本主题:抽象以及重用
  • 不同类型的重用代码
  • 代码重用的优缺点
  • 重用代码的常用策略及指导原则
  • 开放源代码库
  • C++标准库
  • C++程序设计的特定组件

(主要讲述如何利用专业的C++方法进行C++设计,磨刀不误砍柴工,在项目开始时清晰的规划设计实际上可以大幅缩短项目周期)


 

 2.1 程序设计概述

  1. 程序设计(软件设计):为满足程序的功能及性能要求,而实现的结构规范
  2. 设计就是规划如何编写程序
  3. 通常以设计文档的形式写出设计,大多数设计文档的常见布局类似,包括两个主要的部分:
    • 将总的程序分为子系统,包括子系统间的界面及依赖关系、子系统间的数据流、每个子系统的输入输出及通用线程模型;
    • 每个子系统的详情,包括类的细分、类的层次结构、数据结构、算法、具体的线程模型及错误处理的细节

 4. 设计文档通常包括图及表格,以显示子系统交互关系及类层次结构

    • 设计的关键是在写程序前进行思考,而不要一头扎进应用程序
    • 如在写代码时遇到问题,修改设计是不可避免的

 

2.2 程序设计的重要性

  1.  很重要
  2.  程序的子系统并不是孤立存在的——子系统彼此有联系;大部分的设计工作都用来判断和定义这些关系

 

2.3 C++设计的特点

  • 具有庞大的功能集
    • 是C语言的超集,此外还有类和对象、运算符重载、异常、模板及其他功能
  • 是一门面向对象语言
  • 有许多设计通用的、可重复使用代码的工具
  • 提供了有用的标准库
  • 提供了许多设计模式或解决问题的通用方法

 

2.4 C++设计的两个原则

 C++设计的两个基本原则:抽象、重用,这两个原则几乎贯穿高效C++程序设计所有领域

 

2.4.1 抽象

1. 抽象的作用:可以直接使用代码而不必了解底层实现

  • 如:直接调用在 <cmath> 头文件中声明的 sqrt() 函数,而不需要知道该函数求平方根的实际所用算法
  • 如:直接使用 cout 插入运算符 (<<) 已经写好的公有接口进行输出,而不用了解 cout 是怎么将文本输出到屏幕的; cout 的底层实现可以随意改动,只有公开的行为及接口保持不变即可

2. 在设计中使用抽象

  •  例如,一个国际象棋的程序:
    //使用一个指向 ChessPiece 对象的二维指针数组,实现象棋的棋盘
    
    ChessPiece* chessBoard[8][8];
    ChessBoard[0][0] = new Rook();

     

  • 但上面这种方法并没有用到抽象概念,更好的方法是用类建立棋盘模型(这样就可以公开接口,并隐藏底层的实现细节):
    //建立 ChessBoard 类
    
    class ChessBoard
    {
        public:
            // 这里放方法
            void setPieceAt(ChessPiece* piece, int x, int y);
    
            ChessPiece& getPieceAt(int x, int y);
            //getPieceAt()函数返回了一个引用,建议底层实现使用指向对象的指针或智能指针,而不要将对象直接存在集合中,以避免一些奇奇怪怪的bug
    
            bool isEmpty(int x, int y); 
    
        protected:
             // 这里放属性 
    }

     

    注意:接口并不决定底层实现方式,改变实现并不需要改变接口(实现还可以根据需求提供更多的功能)

 

 2.4.2 重用

  •  不必重新造轮子,只需要修改这些轮子并添加必要的功能

 

1. 重用代码

  •  使用已有的代码
  • 如:使用 cout 输出的时候就已经在重用代码了。我们没有编写真正将数据输出到屏幕上的代码,而是使用了已有的 ostream 实现输出

 

2. 编写可供重用的代码

  • 使用类、算法、数据结构...以供重用
  • 在C++中,模板是一种编写多用途代码的语言技术:
    //在此编写了一个可以用于任何二维棋盘游戏类型的泛型GameBoard:
    template <typename PieceType>
    class GameBoard
    {
        public:
            // 这里放方法
            void setPieceAt(PieceType* piece, int x, int y);
            PieceType& getPieceAt(int x, int y);
            bool isEmpty(int x, int y);
    
        protected:
            // 这里放属性
    }

    注意:只需要修改类的声明,在接口中将棋子作为模板参数而不是固定类型

 

2.5 重用代码

  •  好的 C++ 程序员不会从零开始启动一个项目,而是利用各种资源提供的代码(如:标准模板库、开放源代码库、公司专用代码、以前的项目),大胆地重用代码

 

 2.5.1 关于术语的说明

  •  有三种可以重用的代码:
    • 过去编写的代码
    • 同事编写的代码
    • 所在公司以外的第三方编写的代码
  •  可以通过以下几种方式来构建:
    • 独立的函数 / 类
    • 框架
  •  程序使用库,但会适应框架。库提供了特定功能,而框架是程序设计和结构构建的基础
  • 应用程序编程接口(API)是库或代码为特定目的提供的接口(并非库本身,库指的是实现,而 API 指的是库的公开接口)

 

2.5.2 决定是否重用代码

  •  重用代码的优点:
    • 简化设计(被重用的应用程序组件不需要进行设计)
    • 不需要测试(一般认为库代码是没有bug的)
    • 库可以检测用户的错误输入(给出提示)
    • 安全性更高(某领域大佬编写的代码更安全)
    • 库代码会持续改进更新
  •  重用代码的缺点:
    • 需要花时间理解接口及其正确用法
    • 库代码提供的功能与需求功能未必完全吻合
    • 一般来说,库代码的性能可能不会太好,不适应于特定场合,或根本没有对应文档记录
    • 库版本的升级可能会引发新的bug
    • 当产品过于依赖第三方库,而库的开发人员又停止了对库的支持
    • 库还涉及许可证发放问题
    • 还需要考虑跨平台的可移植性(跨平台的应用程序要使用可移植的库)

 

2.5.3 重用代码的策略

  1.  理解功能及限制因素
    • 可以从文档、公开的接口或 API 开始理解代码的运行方式,如果库没有将接口及实现明确分离,可能还要研究源代码
    • 首先应该理解基本功能:  
      • 如果是库,该库可以提供哪些行?
      • 如果是框架,代码如何适应这个框架?
      • 应该编写哪些类的子类?需要自己编写哪些代码
    • 对于任何库或框架:
      • 库或框架需要什么样的初始化调用?需要什么样的清理?
      • 库或框架依赖于其他哪些库?
      • 对于多线程程序而言,代码是否安全?
    • 对于任何库调用:
      • 如果某个调用返回一个内存指针,谁负责内存的释放(调用者还是库)?如果库对此负责,什么时候释放内存?是否可以使用智能指针管理由库分配的内存?
      • 库调用检查哪些错误情况?此时做出了什么假定?如何处理错误?如何提醒客户端程序发生了错误?应避免使用弹出消息对话框、将消息传递到 stderr / cerr 或者 stdout / cout 以及终止程序的库
      • 某个调用的全部返回值(按值或按引用)有哪些?所有可能抛出的异常有哪些?
    • 关于框架:
      • 如果从某个类继承,应该调用哪个构造函数?应该重写哪些虚方法?
      • 程序员负责释放哪些内存?框架负责释放哪些内存?
  2. 理解性能
    • 对代码提供性能保障
  3. 大O表示法
    • 大O表示相对性能而不是绝对性能,将算法的运行时间表示为输入量的函数,就是常说的算法复杂度
      算法复杂度 大O表示法 说明 举例
      常数 O(1) 运行时间与输入量无关 访问数组中的某个元素
      对数 O(logn) 运行时间是输入量对数的函数 使用二分法查找有序列表中的元素
      线性 O(n) 运行时间与输入量成正比 在未排序的列表中查找元素
      线性对数 O(n logn) 运行时间是输入量对数函数的线性倍函数 归并排序
      二次方 O(n2) 运行时间是输入量的平方函数 较慢的排序算法(如选择排序法)
    • 用输入量的函数(而非绝对数字)表示性能的好处:
      • 独立于平台(在不同计算机平台上加载相同负荷也可以显而易见的比较算法性能)
      • 可以用一种方法表示算法所有可能的输入(如果用秒来表示算法所需特定时间,那么该时间只针对于特定输入,对于另外的输入则毫无意义)
  4. 理解性能的几点提示
    • 使用大O表示法表示性能时,需要考虑以下问题:
      • 当数据量加倍时,算法所需的时间也会加倍,如果算法访问了不必要的磁盘,虽然不会影响大O表示法,但性能变得一塌糊涂
      • 因此也很难比较出两个相同大O运行时间的算法孰优孰劣
      • 而对于小规模的输入量,O(n2)算法的实际执行性能可能要优于O(logn)
    • 除了考虑算法的大O特性外,还需要考虑算法性能的其他方面:
      • 重点考虑某段特定库代码的使用频率(90/10法则:90%运行时间花费在10%的代码上)
      • 不要信任文档,一定要运行性能测试来判断库代码是否提供了可接受的性能
  5. 理解平台限制
    • 平台不仅包括不同的操作系统,还包括同一操作系统的不同版本(如:能在8.0版本上运行的库未必能在9.0上运行)
  6. 理解许可证及支持
    • 有时使用第三方供应商提供的库必须支付许可证费用,可能还有其他许可限制
  7. 寻求帮助
    • 首先参考库自带的文档,相关书籍,web(但不一定对)
  8. 原型
    • 首次使用某个新库或框架时,可以编写一个快速原型测试库,以最快熟悉库功能及限制

 

 2.5.4 开放源代码库

  • 开放源代码库是一种日益流行的可重用代码类型
  • 开放源代码(open-source)通常意味着任何人都可以查看源代码

 

2.5.5 C++标准库

  •  标准库并不是整体式库的,它分为几个完全不同的组件
  • C标准库:由于C++是C的超集,因此一些C库的工具在C++中依然有效,包括:
    • 数学函数:abs()、sqrt()、pow()
    • 随机生成数函数:srand()、rand()
    • 错误处理辅助程序:assert()、errno
    • 将字符数组作为字符串操作:strlen()、strcpy()
    • C风格的 I/O 函数:printf()、scanf()
    • 此外还需要注意,C头文件名称与C++不同
  • 判断是否使用STL
    • 设计标准库时优先考虑的是:功能、性能、正交性(orthogonality)
posted @ 2022-11-17 12:21  哟吼--小文文公主  阅读(140)  评论(0编辑  收藏  举报